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 extends GrantedAuthority> 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;
+ }
+}