diff --git a/apis-authorization-server-war/pom.xml b/apis-authorization-server-war/pom.xml index f4adc635..978359ef 100644 --- a/apis-authorization-server-war/pom.xml +++ b/apis-authorization-server-war/pom.xml @@ -19,6 +19,11 @@ nl.surfnet.apis apis-authorization-server + + de.daasi + shib-apis-authn + 0.0.2-SNAPSHOT + com.sun.jersey jersey-servlet @@ -42,6 +47,11 @@ mysql mysql-connector-java + + net.sf.uadetector + uadetector-resources + 2014.04 + com.sun.jersey.contribs jersey-spring diff --git a/apis-authorization-server/pom.xml b/apis-authorization-server/pom.xml index 0990930c..d93606a1 100644 --- a/apis-authorization-server/pom.xml +++ b/apis-authorization-server/pom.xml @@ -17,7 +17,6 @@ apis-authorization-server - jar API Secure - authorization server diff --git a/apis-authorization-server/src/main/java/org/surfnet/oaaas/resource/resourceserver/AccessTokenForOwnerEncryptedResource.java b/apis-authorization-server/src/main/java/org/surfnet/oaaas/resource/resourceserver/AccessTokenForOwnerEncryptedResource.java new file mode 100644 index 00000000..44cfb7ad --- /dev/null +++ b/apis-authorization-server/src/main/java/org/surfnet/oaaas/resource/resourceserver/AccessTokenForOwnerEncryptedResource.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012 SURFnet bv, The Netherlands + * + * 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 org.surfnet.oaaas.resource.resourceserver; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.surfnet.oaaas.model.AccessToken; +import org.surfnet.oaaas.repository.AccessTokenRepository; + +/** + * JAX-RS Resource for maintaining owns access tokens. + */ +@Named +@Path("/accessTokenForOwnerEncrypted") +@Produces(MediaType.APPLICATION_JSON) +public class AccessTokenForOwnerEncryptedResource extends AbstractResource { + + private static final Logger LOG = LoggerFactory.getLogger(AccessTokenForOwnerEncryptedResource.class); + + @Inject + private AccessTokenRepository accessTokenRepository; + + /** + * Get all access token for the provided credentials (== owner). + */ + @GET + public Response getAll(@Context HttpServletRequest request) { + Response validateScopeResponse = validateScope(request, Collections.singletonList(AbstractResource.SCOPE_READ)); + if (validateScopeResponse != null) { + return validateScopeResponse; + } + List tokens = getAllAccessTokens(request); + return Response.ok(tokens).build(); + } + + /** + * Get all tokens for a user. + */ + @GET + @Path("/{accessTokenOwner}") + public Response getByOwner(@Context HttpServletRequest request, @PathParam("accessTokenOwner") String owner) { + Response validateScopeResponse = validateScope(request, Collections.singletonList(AbstractResource.SCOPE_READ)); + if (validateScopeResponse != null) { + return validateScopeResponse; + } + List tokens = getAccessTokensForOwner(request, decode(owner)); + return Response.ok(tokens).build(); + } + + /** + * Delete all existing access tokens for a user. + */ + @DELETE + @Path("/{accessTokenOwner}") + public Response delete(@Context HttpServletRequest request, @PathParam("accessTokenOwner") String owner) { + Response validateScopeResponse = validateScope(request, Collections.singletonList(AbstractResource.SCOPE_WRITE)); + if (validateScopeResponse != null) { + return validateScopeResponse; + } + List tokens = getAccessTokensForOwner(request, decode(owner)); + if (tokens == null || tokens.isEmpty()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + LOG.debug("About to delete accessTokens {}", Arrays.toString(tokens.toArray())); + accessTokenRepository.delete(tokens); + return Response.noContent().build(); + } + + private String decode(String owner) { + try { + owner = URLDecoder.decode(owner, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + LOG.error(String.format("Error while decoding '%s'", owner), e); + } + return owner; +} + + private List getAccessTokensForOwner(HttpServletRequest request, String owner) { + List accessTokens; + String userName = getUserId(request); + if (isAdminPrincipal(request) || owner.equals(userName )) { + accessTokens = accessTokenRepository.findByResourceOwnerId(owner); + LOG.debug("About to return all resource servers ({}) for owner {}", accessTokens.size(), owner); + } else { + accessTokens = new ArrayList<>(); + LOG.debug("User {} is neither admin nor owner. Returning empty list", userName); + } + return accessTokens; + } + + private List getAllAccessTokens(HttpServletRequest request) { + List accessTokens; + if (isAdminPrincipal(request)) { + accessTokens = addAll(accessTokenRepository.findAll().iterator()); + LOG.debug("About to return all resource servers ({}) for adminPrincipal", accessTokens.size()); + } else { + String owner = getUserId(request); + accessTokens = accessTokenRepository.findByResourceOwnerId(owner); + LOG.debug("About to return all resource servers ({}) for owner {}", accessTokens.size(), owner); + } + return accessTokens; + } + + +} diff --git a/apis-authorization-server/src/main/java/org/surfnet/oaaas/resource/resourceserver/AccessTokenForOwnerResource.java b/apis-authorization-server/src/main/java/org/surfnet/oaaas/resource/resourceserver/AccessTokenForOwnerResource.java new file mode 100644 index 00000000..c61a825b --- /dev/null +++ b/apis-authorization-server/src/main/java/org/surfnet/oaaas/resource/resourceserver/AccessTokenForOwnerResource.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012 SURFnet bv, The Netherlands + * + * 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 org.surfnet.oaaas.resource.resourceserver; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.surfnet.oaaas.model.AccessToken; +import org.surfnet.oaaas.repository.AccessTokenRepository; + +/** + * JAX-RS Resource for maintaining owns access tokens. + */ +@Named +@Path("/accessTokenForOwner") +@Produces(MediaType.APPLICATION_JSON) +public class AccessTokenForOwnerResource extends AbstractResource { + + private static final Logger LOG = LoggerFactory.getLogger(AccessTokenForOwnerResource.class); + + @Inject + private AccessTokenRepository accessTokenRepository; + + /** + * Get all access token for the provided credentials (== owner). + */ + @GET + public Response getAll(@Context HttpServletRequest request) { + Response validateScopeResponse = validateScope(request, Collections.singletonList(AbstractResource.SCOPE_READ)); + if (validateScopeResponse != null) { + return validateScopeResponse; + } + List tokens = getAllAccessTokens(request); + return Response.ok(tokens).build(); + } + + /** + * Get all tokens for a user. + */ + @GET + @Path("/{accessTokenOwner}") + public Response getByOwner(@Context HttpServletRequest request, @PathParam("accessTokenOwner") String owner) { + Response validateScopeResponse = validateScope(request, Collections.singletonList(AbstractResource.SCOPE_READ)); + if (validateScopeResponse != null) { + return validateScopeResponse; + } + List tokens = getAccessTokensForOwner(request, owner); + return Response.ok(tokens).build(); + } + + /** + * Delete all existing access tokens for a user. + */ + @DELETE + @Path("/{accessTokenOwner}") + public Response delete(@Context HttpServletRequest request, @PathParam("accessTokenOwner") String owner) { + Response validateScopeResponse = validateScope(request, Collections.singletonList(AbstractResource.SCOPE_WRITE)); + if (validateScopeResponse != null) { + return validateScopeResponse; + } + List tokens = getAccessTokensForOwner(request, owner); + if (tokens == null || tokens.isEmpty()) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + LOG.debug("About to delete accessTokens {}", Arrays.toString(tokens.toArray())); + accessTokenRepository.delete(tokens); + return Response.noContent().build(); + } + + private List getAccessTokensForOwner(HttpServletRequest request, String owner) { + List accessTokens; + String userName = getUserId(request); + if (isAdminPrincipal(request) || owner.equals(userName )) { + accessTokens = accessTokenRepository.findByResourceOwnerId(owner); + LOG.debug("About to return all resource servers ({}) for owner {}", accessTokens.size(), owner); + } else { + accessTokens = new ArrayList<>(); + LOG.debug("User {} is neither admin nor owner. Returning empty list", userName); + } + return accessTokens; + } + + private List getAllAccessTokens(HttpServletRequest request) { + List accessTokens; + if (isAdminPrincipal(request)) { + accessTokens = addAll(accessTokenRepository.findAll().iterator()); + LOG.debug("About to return all resource servers ({}) for adminPrincipal", accessTokens.size()); + } else { + String owner = getUserId(request); + accessTokens = accessTokenRepository.findByResourceOwnerId(owner); + LOG.debug("About to return all resource servers ({}) for owner {}", accessTokens.size(), owner); + } + return accessTokens; + } + + +} diff --git a/pom.xml b/pom.xml index f4c3a765..7393f043 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,7 @@ apis-resource-server-library apis-example-resource-server apis-authorization-server + shib-apis-authn apis-authorization-server-war apis-surfconext-authn apis-example-resource-server-war diff --git a/shib-apis-authn/pom.xml b/shib-apis-authn/pom.xml new file mode 100644 index 00000000..0c192b4b --- /dev/null +++ b/shib-apis-authn/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + ../pom.xml + nl.surfnet.apis + apis-parent + 1.3.6-SNAPSHOT + + + + de.daasi + shib-apis-authn + 0.0.2-SNAPSHOT + API Secure - Shibboleth authentication plugin + + + + org.surfnet.coin + spring-security-opensaml + + + commons-collections + commons-collections + + + + + nl.surfnet.apis + apis-authorization-server + 1.3.6-SNAPSHOT + + + org.surfnet.coin + coin-api-client + + + javax.servlet + javax.servlet-api + + + javax.inject + javax.inject + + + nl.surfnet.apis + apis-resource-server-library + 1.3.6-SNAPSHOT + + + net.sf.uadetector + uadetector-resources + 2014.04 + provided + + + + diff --git a/shib-apis-authn/resources/saml.attributes.properties.dist b/shib-apis-authn/resources/saml.attributes.properties.dist new file mode 100644 index 00000000..acfc756d --- /dev/null +++ b/shib-apis-authn/resources/saml.attributes.properties.dist @@ -0,0 +1,17 @@ +# +# APIs is protected by a Shibboleth SP now, like so: +# +# AuthType shibboleth +# ShibRequestSetting requireSession 1 +# require shib-session +# +# + +# REMOTE_USER is being used as principal. See shibboleth2.xml for which IdP attribute will make it up + +# could list further attributes driving displayname, admin role, etc. +# and implement that in ShibAuthenticator.java + +# Comma separated list of Admin Principals, short of a usable SAML attribute +adminPrincipals=user1@scope.edu,UserName2@anotherscope.edu + diff --git a/shib-apis-authn/src/main/java/de/daasi/shib_apis_authn/SAMLAuthenticatedPrincipal.java b/shib-apis-authn/src/main/java/de/daasi/shib_apis_authn/SAMLAuthenticatedPrincipal.java new file mode 100644 index 00000000..935018b8 --- /dev/null +++ b/shib-apis-authn/src/main/java/de/daasi/shib_apis_authn/SAMLAuthenticatedPrincipal.java @@ -0,0 +1,102 @@ +package de.daasi.shib_apis_authn; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +import org.codehaus.jackson.annotate.JsonIgnore; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.CollectionUtils; +import org.surfnet.oaaas.auth.principal.AuthenticatedPrincipal; + +/** + * essentially the same code as org.surfnet.oaaas.conext.SAMLAuthenticatedPrincipal + * duplicated here in order to not introduce a dependency to conext stuff + */ +public class SAMLAuthenticatedPrincipal extends AuthenticatedPrincipal + implements UserDetails { + + @JsonIgnore + private final static String IDENTITY_PROVIDER = "IDENTITY_PROVIDER"; + + @JsonIgnore + private final static String DISPLAY_NAME = "DISPLAY_NAME"; + + public SAMLAuthenticatedPrincipal() { + } + + public SAMLAuthenticatedPrincipal(String username, + Collection roles, Map attributes, + Collection groups, String identityProvider, + String displayName, boolean adminPrincipal) { + super(username, roles, attributes, groups); + addAttribute(IDENTITY_PROVIDER, identityProvider); + addAttribute(DISPLAY_NAME, displayName); + setAdminPrincipal(adminPrincipal); + } + + @JsonIgnore + @Override + public Collection getAuthorities() { + ArrayList authorities = new ArrayList(); + if (!CollectionUtils.isEmpty(getRoles())) { + for (final String role : getRoles()) { + authorities.add(new GrantedAuthority() { + public String getAuthority() { + return role; + } + }); + } + } + return authorities; + } + + @JsonIgnore + @Override + public String getPassword() { + throw new RuntimeException( + "SAML based authentication does not support passwords on the receiving end"); + } + + @JsonIgnore + @Override + public String getUsername() { + return getName(); + } + + @Override + public String getDisplayName() { + return getAttributes().get(DISPLAY_NAME); + } + + @JsonIgnore + @Override + public boolean isAccountNonExpired() { + return true; + } + + @JsonIgnore + @Override + public boolean isAccountNonLocked() { + return true; + } + + @JsonIgnore + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @JsonIgnore + @Override + public boolean isEnabled() { + return true; + } + + @JsonIgnore + public String getIdentityProvider() { + return getAttributes().get(IDENTITY_PROVIDER); + } + +} diff --git a/shib-apis-authn/src/main/java/de/daasi/shib_apis_authn/ShibAuthenticator.java b/shib-apis-authn/src/main/java/de/daasi/shib_apis_authn/ShibAuthenticator.java new file mode 100644 index 00000000..92ba2d0b --- /dev/null +++ b/shib-apis-authn/src/main/java/de/daasi/shib_apis_authn/ShibAuthenticator.java @@ -0,0 +1,110 @@ +/* + * Copyright 2014, Martin Haase, DAASI International, Germany + * + * based on org.surfnet.oaaas.conext.SAMLAuthenticator and heavily reduced + * (essentially removed homegrown spring security based SP and group api stuff) + * + * 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 de.daasi.shib_apis_authn; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.stereotype.Component; +import org.surfnet.oaaas.auth.AbstractAuthenticator; + +@Component +public class ShibAuthenticator extends AbstractAuthenticator { + +// private static final Logger LOG = LoggerFactory +// .getLogger(ShibAuthenticator.class); + + private List adminList; + + private final Properties properties; + + { + try { + // Use Remote_user for Principal; only set Admin uids here; could extend by more + // attribute mappings, e.g. for DISPLAYNAME or roles. + properties = PropertiesLoaderUtils + .loadAllProperties("saml.attributes.properties"); + String[] admins = properties.getProperty("adminPrincipals") + .toLowerCase().split("\\s*,\\s*"); + adminList = Arrays.asList(admins); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + try { + super.init(filterConfig); + } catch (Exception e) { + throw new ServletException(e); + } + } + + @Override + public boolean canCommence(HttpServletRequest request) { + return false; + } + + @Override + public void authenticate(HttpServletRequest request, + HttpServletResponse response, FilterChain chain, + String authStateValue, String returnUri) throws IOException, + ServletException { +// LOG.debug("OAuthCallback: " + isOAuthCallback(request)); + + String userId = request.getRemoteUser(); + if (StringUtils.isEmpty(userId)) { + throw new ServletException("No REMOTE_USER from Shibboleth SP!"); + } + boolean isAdmin = false; + if (adminList.contains(userId.toLowerCase())) { + isAdmin = true; + } + + // TODO: could populate SAML attributes + // HashMap attributes = new HashMap(); + + // populate User Agent details + UserAgent userAgent = new UserAgent(request); + HashMap attributes = userAgent.getAttributes(); + + SAMLAuthenticatedPrincipal principal = new SAMLAuthenticatedPrincipal( + userId, new ArrayList(), attributes, + new ArrayList(), null, userId, isAdmin); + + super.setPrincipal(request, principal); + super.setAuthStateValue(request, authStateValue); + chain.doFilter(request, response); + } + +} diff --git a/shib-apis-authn/src/main/java/de/daasi/shib_apis_authn/UserAgent.java b/shib-apis-authn/src/main/java/de/daasi/shib_apis_authn/UserAgent.java new file mode 100644 index 00000000..aca19be5 --- /dev/null +++ b/shib-apis-authn/src/main/java/de/daasi/shib_apis_authn/UserAgent.java @@ -0,0 +1,39 @@ +package de.daasi.shib_apis_authn; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; + +import net.sf.uadetector.ReadableUserAgent; +import net.sf.uadetector.UserAgentStringParser; +import net.sf.uadetector.service.UADetectorServiceFactory; + +public class UserAgent { + + private HashMap attributes; + + public UserAgent (HttpServletRequest request) { + + UserAgentStringParser parser = UADetectorServiceFactory.getResourceModuleParser(); + ReadableUserAgent agent = parser.parse(request.getHeader("User-Agent")); + + attributes = new HashMap(); + attributes.put("platform", agent.getOperatingSystem().getName()); + attributes.put("model", agent.getVersionNumber().toVersionString()); + attributes.put("useragent", agent.getName()); + + Enumeration locales = request.getLocales(); + if (locales.hasMoreElements()) { // do not use 'while' because we only need the first language available + Locale firstLocale = (Locale) locales.nextElement(); + attributes.put("locale", firstLocale.toString()); // use full string de_DE_xxx_yyy + } else { + attributes.put("locale", "unknown"); + } + } + + public HashMap getAttributes () { + return attributes; + } +}