Skip to content
This repository has been archived by the owner on Jul 3, 2020. It is now read-only.

Commit

Permalink
Added implementation and test for only asking for consent once
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Aug 14, 2012
1 parent 141daca commit 2f068ca
Show file tree
Hide file tree
Showing 24 changed files with 456 additions and 341 deletions.
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
apis
======
The apis (APIs Secure) project offers an OAuth 2.0 Authorization Server that can be used to kickstart your API authentication. In essence it enables you to focus on your actual resource endpoints and use the out-of-the-box authorization server to authenticate resource owners and subsequently validate the access tokens that were granted to the Client applications. We will offer more details in later sections.
The apis (APIs Secure) project offers an OAuth 2.0 Authorization Server that can be used to kickstart your API authentication. In essence it enables you to focus on your actual resource endpoints and use the out-of-the-box authorization server to authenticate resource owners and subsequently validate the access tokens that were granted to the Client applications. We will describe the typical use cases in more details in sections below.

## Features

- An OAuth2 Authorization Server compliant with [the draft v2-30 specification](http://tools.ietf.org/html/draft-ietf-oauth-v2-30)
- An OAuth2 Authorization Server compliant with [the draft v2-31 specification](http://tools.ietf.org/html/draft-ietf-oauth-v2-31)
* Pluggable authentication and userConsent handling (with default implementations provided)
* Support for authorization code and implicit grant
* GUI included for the registration of Resource Servers and Client apps
Expand All @@ -29,11 +29,30 @@ Go the authorization-server-war and start the application
cd apis-authorization-server-war
mvn jetty:run

The authorization-server-war application is capable of authenticating users and granting access tokens (and optional refresh tokens). It also offers a JavaScript application to manage Resource Servers and Client application instances.
The authorization-server-war application is capable of authenticating Resource Owners and granting and validating Access Tokens (and optional Refresh Tokens) on behalf of Resource Servers that are receiving resource calls from a Client app. It also offers a JavaScript application to manage Resource Servers and Client application instances.

### Run Example Resource Server
### Run Example Resource Server (war & standalone modus)

Go to the
We have provided two example resource servers. One (apis-example-resource-server-war) is a very simple Java web application
that only demonstrates how a Resource Server can communicate with the Authorization Server using the `org.surfnet.oaaas.auth.AuthorizationServerFilter` (which is a simple `javax.servlet.Filter`). The `AuthorizationServerFilter` only protects a single JSP page in the apis-example-resource-server-war module.

The other example resource server (apis-example-resource-server) is build using [Dropwizard](http://dropwizard.codahale.com/). We will start this one to demonstrate the entire flow (new Terminal session):

cd apis-example-resource-server-war
java -jar target/apis-example-resource-server-0.1-SNAPSHOT.jar

### Run Example Client App

We have now an Authorization Server running and an example Resource Server (and we have not much to show for it, yet!). To demonstrate the entire flow we will start an example Client Application which will communicate with:

- first the Authorization Server to obtain an Access Token
* Note that this only works because we have configured both the example-resource-server and the example-client-app in the dummy data defined in /apis-authorization-server/src/main/resources/db/migration/hsqldb/V1__auth-server-admin.sql
- then the example Resource Server to make an REST API call using the obtained OAuth Access Token
* Note that the example Resource Server communicates with the Authorization Server to validate the token

Start the example-client-app (new Terminal session)

cd

### Resource Servers and Client apps GUI registration

Expand Down
3 changes: 0 additions & 3 deletions apis-authorization-server-war/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@

<properties>
<servlet.port>8080</servlet.port>
<jetty-maven-plugin.version>8.1.4.v20120524</jetty-maven-plugin.version>
<selenium.version>2.25.0</selenium.version>
</properties>

<dependencies>
Expand All @@ -24,7 +22,6 @@
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-servlet</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,25 @@
package org.surfnet.oaaas.consent;

import java.io.IOException;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.HttpMethod;
import org.surfnet.oaaas.auth.AbstractAuthenticator;
import org.surfnet.oaaas.auth.AbstractUserConsentHandler;
import org.surfnet.oaaas.auth.principal.AuthenticatedPrincipal;
import org.surfnet.oaaas.model.AccessToken;
import org.surfnet.oaaas.model.Client;
import org.surfnet.oaaas.repository.AccessTokenRepository;

/**
* Example {@link AbstractUserConsentHandler} that forwards to a form.
Expand All @@ -41,6 +48,9 @@ public class FormUserConsentHandler extends AbstractUserConsentHandler {

private static final String USER_OAUTH_APPROVAL = "user_oauth_approval";

@Inject
private AccessTokenRepository accessTokenRepository;

/*
* (non-Javadoc)
*
Expand All @@ -54,10 +64,11 @@ public class FormUserConsentHandler extends AbstractUserConsentHandler {
public void handleUserConsent(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
String authStateValue, String returnUri, Client client) throws IOException, ServletException {
if (isUserConsentPost(request)) {
processForm(request);
chain.doFilter(request, response);
if (processForm(request, response)) {
chain.doFilter(request, response);
}
} else {
processInitial(request, response, returnUri, authStateValue, client);
processInitial(request, response, chain, returnUri, authStateValue, client);
}
}

Expand All @@ -66,12 +77,19 @@ private boolean isUserConsentPost(HttpServletRequest request) {
return request.getMethod().equals(HttpMethod.POST.toString()) && StringUtils.isNotBlank(oauthApproval);
}

private void processInitial(HttpServletRequest request, ServletResponse response, String returnUri,
String authStateValue, Client client) throws IOException, ServletException {
request.setAttribute("client", client);
request.setAttribute(AUTH_STATE, authStateValue);
request.setAttribute("actionUri", returnUri);
request.getRequestDispatcher(getUserConsentUrl()).forward(request, response);
private void processInitial(HttpServletRequest request, ServletResponse response, FilterChain chain,
String returnUri, String authStateValue, Client client) throws IOException, ServletException {
AuthenticatedPrincipal principal = (AuthenticatedPrincipal) request.getAttribute(AbstractAuthenticator.PRINCIPAL);
List<AccessToken> tokens = accessTokenRepository.findByResourceOwnerIdAndClient(principal.getName(), client);
if (!CollectionUtils.isEmpty(tokens)) {
chain.doFilter(request, response);
} else {
request.setAttribute("client", client);
request.setAttribute(AUTH_STATE, authStateValue);
request.setAttribute("actionUri", returnUri);
request.getRequestDispatcher(getUserConsentUrl()).forward(request, response);
}

}

/**
Expand All @@ -85,12 +103,24 @@ protected String getUserConsentUrl() {
return "/WEB-INF/jsp/userconsent.jsp";
}

private void processForm(final HttpServletRequest request) {
private boolean processForm(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
if (Boolean.valueOf(request.getParameter(USER_OAUTH_APPROVAL))) {
setAuthStateValue(request, request.getParameter(AUTH_STATE));
String[] scopes = request.getParameterValues(GRANTED_SCOPES);
setScopes(request, scopes);
return true;
} else {
request.getRequestDispatcher(getUserConsentDeniedUrl()).forward(request, response);
return false;
}
}

/**
* @return
*/
protected String getUserConsentDeniedUrl() {
return "/WEB-INF/jsp/userconsent_denied.jsp";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<img src="/client/img/dead-end-sign.jpg"/>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.config;

import javax.inject.Inject;

import org.apache.openjpa.persistence.PersistenceProviderImpl;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.surfnet.oaaas.repository.ExceptionTranslator;
import org.surfnet.oaaas.repository.OpenJPAExceptionTranslator;

@Configuration
@PropertySource("classpath:apis.application.test.properties")
/*
* The component scan can be used to add packages and exclusions to the default
* package
*/
@ImportResource("classpath:spring-repositories.xml")
@EnableTransactionManagement
public class TestSpringConfiguration {

private static final String PERSISTENCE_UNIT_NAME = "oaaas";
private static final Class<PersistenceProviderImpl> PERSISTENCE_PROVIDER_CLASS = PersistenceProviderImpl.class;

@Inject
Environment env;

@Bean
public javax.sql.DataSource dataSource() {
DataSource dataSource = new DataSource();
dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.username"));
dataSource.setPassword(env.getProperty("jdbc.password"));
return dataSource;
}

@Bean
public JpaTransactionManager transactionManager() {
return new JpaTransactionManager();
}

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
final LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean();
emfBean.setDataSource(dataSource());
emfBean.setPersistenceUnitName(PERSISTENCE_UNIT_NAME);
emfBean.setPersistenceProviderClass(PERSISTENCE_PROVIDER_CLASS);
return emfBean;
}

@Bean
public ExceptionTranslator exceptionTranslator() {
return new OpenJPAExceptionTranslator();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,34 @@

package org.surfnet.oaaas.selenium;

import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.matchers.JUnitMatchers.containsString;

import java.net.URI;

import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertThat;
import static org.junit.matchers.JUnitMatchers.containsString;

/**
* Integration test (using Selenium) for the Implicit Grant flow.
*/
public class ImplicitGrantTestIT extends SeleniumSupport {

@Test
public void implicitGrant() {
performImplicitGrant(true);
/*
* The second time no consent is required (as we have already an access token for the client/ principal name
*/
performImplicitGrant(false);
}

private void performImplicitGrant(boolean needConsent) {
WebDriver webdriver = getWebDriver();

String responseType = "token";
String clientId = "it-test-client";
String clientId = "it-test-client-grant";
String redirectUri = "http://localhost:8080/fourOhFour";

String url = String.format(
Expand All @@ -45,7 +52,7 @@ public void implicitGrant() {
webdriver.get(url);
assertThat(webdriver.getPageSource(), containsString("Login with your identifier and password"));

login(webdriver, true);
login(webdriver, needConsent);

// Token response
URI responseURI = URI.create(webdriver.getCurrentUrl());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,11 @@

import org.apache.http.localserver.LocalTestServer;
import org.codehaus.jackson.map.ObjectMapper;
import org.junit.Before;
import org.junit.BeforeClass;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.surfnet.oaaas.auth.OAuth2Validator;
import org.surfnet.oaaas.auth.ObjectMapperProvider;
import org.surfnet.oaaas.it.AbstractAuthorizationServerTest;
Expand All @@ -49,8 +46,8 @@ public abstract class SeleniumSupport extends AbstractAuthorizationServerTest {

private ObjectMapper mapper = new ObjectMapperProvider().getContext(ObjectMapper.class);

@Before
public void initializeOnce() {
@BeforeClass
public static void initializeOnce() {
if (driver == null) {
if ("firefox".equals(System.getProperty("selenium.webdriver", "firefox"))) {
initFirefoxDriver();
Expand Down Expand Up @@ -79,15 +76,15 @@ protected AuthorizationCodeRequestHandler getAuthorizationCodeRequestHandler() {
return authorizationCodeRequestHandler;
}

private void initHtmlUnitDriver() {
private static void initHtmlUnitDriver() {
initDriver(new HtmlUnitDriver());
}

private void initFirefoxDriver() {
private static void initFirefoxDriver() {
initDriver(new FirefoxDriver());
}

private void initDriver(WebDriver remoteWebDriver) {
private static void initDriver(WebDriver remoteWebDriver) {
SeleniumSupport.driver = remoteWebDriver;
SeleniumSupport.driver.manage().timeouts().implicitlyWait(3, TimeUnit.SECONDS);
Runtime.getRuntime().addShutdownHook(new Thread() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

# The database settings
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:file:target/db;shutdown=true
jdbc.url=jdbc:hsqldb:file:target/db;shutdown=true;hsqldb.lock_file=false
jdbc.username=sa
jdbc.password=

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#
# 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.
#

# The database settings
jdbc.driverClassName=org.hsqldb.jdbcDriver
#jdbc.url=jdbc:hsqldb:hsq://localhost/xdb
#jdbc.url=jdbc:hsqldb:hsql//localhost/target/db;ifexists=false
jdbc.url=jdbc:hsqldb:file:target/db;hsqldb.lock_file=false
jdbc.username=sa
jdbc.password=
Loading

0 comments on commit 2f068ca

Please sign in to comment.