Merge branch 'master' into userInfoEndpoint

pull/105/merge
Stephen Moore 2012-05-23 13:43:32 -04:00
commit 5c544dfe7c
36 changed files with 1762 additions and 776 deletions

View File

@ -2,53 +2,60 @@
## Overview
This is Web application created in response to [Issue #39] to permit the Client AuthenticationFilter to speak to multiple OpenID Connect servers.
This is Web application created in response to [Issue #39] to permit the Client AuthenticationFilter to speak to multiple OpenID Connect Servers. The protocol between the Clinent and the Account Chooser UI application is documented the README.md of the openid-connect-client submodule.
## Configuration
Configure a bean configuration to the spring-servlet.xml like so:
Configure AccountChooserController via configuring a AccountChooserConfig bean in the spring-servlet.xml like so:
<bean class="org.mitre.account_chooser.OIDCServers">
<property name="servers">
<bean name="AccountChooserConfig" class="org.mitre.account_chooser.AccountChooserConfig">
<property name="issuers">
<map>
<entry key="1">
<entry key="http://sever.example.com:8080/openid-connect-server">
<bean class="org.mitre.account_chooser.OIDCServer">
<property name="name" value="OIDC Server 1" />
</bean>
</entry>
<entry key="2">
<bean class="org.mitre.account_chooser.OIDCServer">
<property name="name" value="OIDC Server 2" />
</bean>
</entry>
<entry key="3">
<bean class="org.mitre.account_chooser.OIDCServer">
<property name="name" value="OIDC Server 3" />
<property name="name" value="Example Server" />
</bean>
</entry>
</map>
</property>
<property name="validClientIds" value="FGWEUIASJK, IUYTTYEV, GFHDSFYD" />
</bean>
The keys must match those found in the OpenIdConnectAuthenticationFilter's configuration like so:
<bean id="openIdConnectAuthenticationFilter"
class="org.mitre.openid.connect.client.OpenIdConnectAuthenticationFilter">
<property name="OIDCServers">
<property name="errorRedirectURI" value="/login.jsp?authfail=openid" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="accountChooserURI"
value="http://sever.example.com:8080/account-chooser" />
<property name="accountChooserClientID" value="FGWEUIASJK" />
<property name="oidcServerConfigs">
<map>
<entry key="1">
<property name="authorizationEndpointURI"
value="http://sever.example.com:8080/openid-connect-server/oauth/authorize" />
<property name="tokenEndpointURI"
value="http://sever.example.com:8080/openid-connect-server/oauth/token" />
<property name="checkIDEndpointURI"
value="http://sever.example.com:8080/openid-connect-server/checkid" />
<property name="clientId"
value="someClientId" />
<property name="clientSecret" value="someClientSecret" />
<entry key="http://sever.example.com:8080/Fopenid-connect-server">
<bean class="org.mitre.openid.connect.client.OIDCServerConfiguration">
<property name="authorizationEndpointURI"
value="http://sever.example.com:8080/openid-connect-server/oauth/authorize" />
<property name="tokenEndpointURI"
value="http://sever.example.com:8080/openid-connect-server/oauth/token" />
<property name="checkIDEndpointURI"
value="http://sever.example.com:8080/openid-connect-server/checkid" />
<property name="clientId"
value="someClientId" />
<property name="clientSecret" value="someClientSecret" />
</bean>
</entry>
. . .
[Issue #39]: http://github.com/jricher/OpenID-Connect-Java-Spring-Server/issues/39 "Issue #39 -- Multiple Point Client"
## Test the Default Configuration
To test the default config, deploy to a servlet container, and request:
http://localhost:8080/account-chooser/?redirect_uri=http://www.google.com&client_id=FGWEUIASJK
Click **Submit** or **Cancel**, and Google will open. Study the URL parameters of each.
[Issue #39]: http://github.com/jricher/OpenID-Connect-Java-Spring-Server/issues/39 "Issue #39 -- Multiple Point Client"

View File

@ -0,0 +1 @@
protocol.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -0,0 +1,127 @@
# OpenID Connect Client - Account Chooser UI Application Protocol
## Overview
This Client - Account Chooser Protocol proposed in response to [Issue #39].
#### Authorization when using Account Chooser Code Flow
The Authorization when using Account Chooser Code Flow goes through the following steps:
1. Client prepares an Account Chooser Request containing the desired request parameters.
2. Client sends a request to the Account Chooser.
3. Account Chooser presents a selection of OpenID Connect (OIDC) Servers from which the End-User must select from.
4. End-User selects an OIDC.
5. Account Chooser Sends the End-User back to the Client with key value of the OIDC End-User selected.
6. The Client begins the Authorization flow described in [Authorization Code Flow][OpenID Connect Standard] of the [OpenID Connect Standard].
#### Account Chooser Request
When the End-User wishes to access a Protected Resource and the End-User Authorization has not yet been obtained, the Client will redirect the End-User to Account Chooser.
Account Chooser MUST support the use of the HTTP "GET" and "POST" methods defined in RFC 2616 [RFC2616].
Clients MAY use the HTTP "GET" or "POST" method to send the Account Chooser Request to the Account Chooser. If using the HTTP "GET" method, the request parameters are serialized using URI query string serialization. If using the HTTP "POST" method, the request parameters are serialized using form serialization.
#### Client Prepares an Account Chooser Request
The Client prepares an Account Chooser Request to the Account Chooser with the request parameters using the HTTP "GET" or "POST" method.
The required Account Chooser Request parameters are as follows:
* redirect_uri - REQUIRED. A redirection URI where the response will be sent.
* client_id - REQUIRED. An identifier used to identify the Client to the Account Chooser UI application.
There is one method to construct and send the request to the Account Chooser:
* Simple Request Method
#### Simple Request Method
The Client prepares an Account Chooser Request to the Account Chooser using the appropriate parameters. If using the HTTP "GET" method, the request parameters are serialized using URI query string serialization. If using the HTTP "POST" method, the request parameters are serialized using form serialization.
The following is a non-normative example of an Account Chooser Request URL. Line wraps are for display purposes only.
http://server.example.com/chooser?
redirect_uri=https%3A%2F%2Fclient.example.com%2Fopenid_connect_login
&client_id=FGWEUIASJK
#### Client sends a request to the Account Chooser
Having constructed the Account Chooser Request, the Client sends it to the Account Chooser. This MAY happen via redirect, hyperlinking, or any other means of directing the User-Agent to the Account Chooser URL.
Following is a non-normative example using HTTP redirect. Line wraps are for display purposes only.
HTTP/1.1 302 Found
Location: https://server.example.com/chooser?
redirect_uri=https%3A%2F%2Fclient.example.com%2Fopenid_connect_login
&client_id=FGWEUIASJK
#### Account Chooser Sends the End-User back to the Client
After the End-User has select an OpenID Connect Server, the Client issues an Account Chooser Response and delivers it to the Client by adding the response parameters to redirect_uri specified in the Account Choose Request using the "application/x-www-form-urlencoded" format.
The following response parameters are included:
* issuer - REQUIRED. The [Issuer Identifier] describing the selected account.
The following is non-normative example of a responses. Line wraps are for display purposes only.
HTTP/1.1 302 Found
Location: https://client.example.com/openid_connect_login?
issuer=http%3A%2F%2Fsever.example.com%3A8080%2Fopenid-connect-server
A sequence diagram of a successful interaction would be:
![Success](https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/raw/master/account-chooser/docs/success.png)
#### The Client ID is not Supported by the Account Chooser Application
If the Client sends a Client identifier that is not supported by the Account Chooser UI application, the Account Chooser MUST return an error response back to the End-User by issuing a HTTP Response with a message concerning the error. The Account Chooser MUST NOT directly return to the Client via the redirection URI specified in the Account Chooser Request automatically.
The error message the Account Chooser presents to the End-User MAY give the End-User the means to return the indication of an error to the Client, e.g., via the End-User clicking on a HTML link built from adding the error parameters to the query component of the redirection URI. No other parameters SHOULD be returned.
The error response parameters are the following:
* error - REQUIRED. The error code.
* error_description - OPTIONAL. A human-readable UTF-8 encoded text description of the error.
The response parameters are added to the query component of the redirection URI specified in the Account Chooser Request.
The following is a non-normative example. Line wraps after the second line are for the display purposes only.
https://client.example.com/openid_connect_login?
error=not_supported
&error_description=The%20client_id%20is%20not%20supported%20by%20the%20AccountChooser%20UI%20application.
A sequence diagram of the interaction where the Account Chooser was not configured to support the Client would be:
![Not supported](https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/raw/master/account-chooser/docs/not_supported.png)
#### End-User refuses to select an Account
If the End-User refuses to select an Account, the Account Chooser MUST return an error response. The Account Chooser returns the Client via the redirection URI specified in the Account Chooser Request with the appropriate error parameters. No other parameters SHOULD be returned.
The error response parameters are the following:
* error - REQUIRED. The error code.
* error_description - OPTIONAL. A human-readable UTF-8 encoded text description of the error.
The response parameters are added to the query component of the redirection URI specified in the Account Chooser Request.
The following is a non-normative example. Line wraps after the second line are for the display purposes only.
HTTP/1.1 302 Found
Location: https://client.example.com/openid_connect_login?
error=end_user_cancelled
&error_description=The%20end%2Duser%20refused%20to%20select%20an%20Account.
A sequence diagram of an interaction where the End-User refused to select an Account would be:
![Cancel](https://github.com/mitreid-connect/OpenID-Connect-Java-Spring-Server/raw/master/account-chooser/docs/cancel.png)
[OpenID Connect Standard]: http://openid.net/specs/openid-connect-standard-1_0.html "OpenID Connect Standard 1.0"
[OpenID Connect Standard]: http://openid.net/specs/openid-connect-standard-1_0.html#code_flow "Authorization Code Flow, OpenID Connect Standard"
[Issuer Identifier]: http://openid.net/specs/openid-connect-messages-1_0.html#issuer_identifier "Issuer Identifier"
[Issue #39]: http://github.com/jricher/OpenID-Connect-Java-Spring-Server/issues/39 "Issue #39 -- Multiple Point Client"

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,50 @@
Text used to create sequence diagrams @ http://www.websequencediagrams.com/
A Successful Client and Account Chooser Interaction
Client->End-User Browser: HTTP-Redirect\n to Account\n Chooser URL\nwith redirect_uri,\n and client_id\n parameters
activate End-User Browser
End-User Browser->Account Chooser:HTTP Get/Put\n Request containing\n redirect_uri\n and client_id
note right of "Account Chooser": Client Id\n is verified as\n supported, and the\n End-User selects\n an Account
deactivate End-User Browser
activate Account Chooser
Account Chooser->End-User Browser:HTTP-Redirect to \nAccount Chooser URL\n with issuer parameters
activate End-User Browser
deactivate Account Chooser
End-User Browser->Client: HTTP Get\n Request containing the\n issuer parameter
activate Client
deactivate End-User Browser
The Client ID is not Supported by the Account Chooser Application
Client->End-User Browser: HTTP-Redirect to\n Account Chooser URL\n w/ redirect_uri,\n and client_id parameters
activate End-User Browser
End-User Browser->Account Chooser:HTTP Get/Put\n Request containing\n redirect_uri and client_id
note right of "Account Chooser": Client Id is\nnot supported.\nSo, an error must\nbe returned.
activate Account Chooser
deactivate End-User Browser
Account Chooser->End-User Browser:Returns an\nHTTP Response\ncontaining an\nerror message.
deactivate Account Chooser
note right of "End-User Browser":The End-User is presented\nan error message\nthus ending the interaction.\n\nOptionally, the message\nMAY contain a HTML link\nto return the End-User\nback to the Client with\nan error and error description\nadded to the query component\nof the redirect_uri.
activate End-User Browser
End-User Browser->Client: If the End-User\nis presented and clicks\non a HTML link,\na HTTP "GET"\n Request containing the\nerror, and error_description\nparameter
activate Client
deactivate Account Chooser
End-User Cancels Account Selection
Client->End-User Browser: HTTP-Redirect to\n Account Chooser URL\nwith redirect_uri,\nand client_id parameters
activate End-User Browser
End-User Browser->Account Chooser:HTTP Get/Put\n Request containing\nredirect_uri and client_id
note right of "Account Chooser": End-User refuses\nto select an\naccount via\ncancelling.
deactivate End-User Browser
activate Account Chooser
Account Chooser->End-User Browser:HTTP-Redirect\n to Account Chooser\n URL with error,\nand error_description\nparameters
activate End-User Browser
deactivate Account Chooser
End-User Browser->Client: HTTP Get Request\ncontaining the error,\nand error_description\nparameter
activate Client
deactivate End-User Browser

View File

@ -0,0 +1,49 @@
/*******************************************************************************
* Copyright 2012 The MITRE Corporation
*
* 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.mitre.account_chooser;
import java.util.HashMap;
import java.util.Map;
/**
* Used to the configure AccountChooserController
*
* @author nemonik
*
*/
public class AccountChooserConfig {
private String[] validClientIds;
private Map<String, ? extends OIDCServer> issuers = new HashMap<String, OIDCServer>();
public Map<String, ? extends OIDCServer> getIssuers() {
return issuers;
}
public String[] getValidClientIds() {
return validClientIds;
}
public void setIssuers(Map<String, ? extends OIDCServer> issuers) {
this.issuers = issuers;
}
public void setValidClientIds(String[] validClientIds) {
this.validClientIds = validClientIds;
}
}

View File

@ -16,6 +16,11 @@
package org.mitre.account_chooser;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
@ -33,14 +38,49 @@ import org.springframework.web.servlet.ModelAndView;
*
* @author nemonik
*
* See README.md for configuration.
* See README.md for configuration.
*
*/
@Controller
public class AccountChooserController {
/**
* Return the URL w/ GET parameters
*
* @param baseURI
* A String containing the protocol, server address, path, and
* program as per "http://server/path/program"
* @param queryStringFields
* A map where each key is the field name and the associated
* key's value is the field value used to populate the URL's
* query string
* @return A String representing the URL in form of
* http://server/path/program?query_string from the messaged
* parameters.
*/
public static String buildURL(String baseURI,
Map<String, String> queryStringFields) {
StringBuilder URLBuilder = new StringBuilder(baseURI);
char appendChar = '?';
for (Map.Entry<String, String> param : queryStringFields.entrySet()) {
try {
URLBuilder.append(appendChar).append(param.getKey())
.append('=')
.append(URLEncoder.encode(param.getValue(), "UTF-8"));
} catch (UnsupportedEncodingException uee) {
throw new IllegalStateException(uee);
}
appendChar = '&';
}
return URLBuilder.toString();
}
@Autowired
OIDCServers servers;
AccountChooserConfig accountChooserConfig;
private static Log logger = LogFactory
.getLog(AccountChooserController.class);
@ -51,18 +91,51 @@ public class AccountChooserController {
* @param redirectUri
* A redirection URI where the response will be sent
* @return
* @throws IOException
*/
@RequestMapping(value = "/", method = { RequestMethod.GET,
RequestMethod.POST })
public ModelAndView handleChooserRequest(
@RequestParam("redirect_uri") String redirectUri) {
@RequestParam("redirect_uri") String redirectUri,
@RequestParam("client_id") String clientId,
HttpServletResponse response) throws IOException {
ModelAndView modelAndView = new ModelAndView("form");
modelAndView.addObject("servers", servers);
modelAndView.addObject("redirect_uri", redirectUri);
modelAndView.setViewName("chooser");
ModelAndView modelAndView = null;
if (Arrays.asList(accountChooserConfig.getValidClientIds()).contains(
clientId)) {
// client_id supported
modelAndView = new ModelAndView("chooser");
modelAndView
.addObject("issuers", accountChooserConfig.getIssuers());
modelAndView.addObject("redirect_uri", redirectUri);
modelAndView.addObject("client_id", clientId);
} else {
// client_id not supported
Map<String, String> urlVariables = new HashMap<String, String>();
urlVariables.put("error", "not_supported");
urlVariables
.put("error_description",
"The client_id is not supported by the Account Chooser UI application.");
modelAndView = new ModelAndView("error");
modelAndView.addObject("error", urlVariables.get("error"));
modelAndView.addObject("error_description",
urlVariables.get("error_description"));
modelAndView.addObject("client_uri", AccountChooserController
.buildURL(redirectUri, urlVariables));
}
return modelAndView;
}
/**
@ -81,9 +154,50 @@ public class AccountChooserController {
*/
@RequestMapping(value = "/selected")
public void processSubmit(@RequestParam("redirect_uri") String redirectUri,
@RequestParam("alias") String alias, HttpServletResponse response)
@RequestParam("issuer") String issuer,
@RequestParam("client_id") String clientId,
HttpServletResponse response)
throws IOException {
response.sendRedirect(redirectUri + "?oidc_alias=" + alias);
// Handle Submit
Map<String, String> urlVariables = new HashMap<String, String>();
urlVariables.put("issuer", issuer);
response.sendRedirect(AccountChooserController.buildURL(redirectUri,
urlVariables));
}
/**
* Handles form submits
*
* @param redirectUri
* A redirection URI where the response will be sent.
* @param alias
* The OIDC alias selected.
* @param response
* Provide the HTTP-specific functionality for sending a
* response. In this case a redirect to redirect the End-User
* back to the OpenID Connect Client.
* @throws IOException
* If an output exception occurs in sending the redirect.
*/
@RequestMapping(value = "/cancel")
public void processCancel(@RequestParam("redirect_uri") String redirectUri,
@RequestParam("issuer") String issuer,
@RequestParam("client_id") String clientId,
HttpServletResponse response)
throws IOException {
// Handle Cancel
Map<String, String> urlVariables = new HashMap<String, String>();
urlVariables.put("error", "end_user_cancelled");
urlVariables.put("error_description",
"The end-user refused to select an Account.");
response.sendRedirect(AccountChooserController.buildURL(redirectUri,
urlVariables));
}
}

View File

@ -18,6 +18,8 @@ package org.mitre.account_chooser;
/**
* @author nemonik
*
* Holdes properties of the Issuer, e.g. name, icon URL, description, et cetera
*
*/
public class OIDCServer {

View File

@ -1,77 +0,0 @@
/*******************************************************************************
* Copyright 2012 The MITRE Corporation
*
* 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.mitre.account_chooser;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
/**
* @author nemonik
*
*/
public class OIDCServers implements InitializingBean {
private Map<String, ? extends OIDCServer> servers = new HashMap<String, OIDCServer>();
private static Log logger = LogFactory.getLog(OIDCServers.class);
/*
* (non-Javadoc)
*
* @see
* org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() throws Exception {
// used for debugging...
if (!servers.isEmpty()) {
logger.info(this.toString());
}
}
/**
* Return the OIDCServers associated with this
*
* @return
*/
public Map<String, ? extends OIDCServer> getServers() {
return servers;
}
/**
* Set the OIDCServers associated with this
*
* @param signers
* List of JwtSigners to associate with this service
*/
public void setServers(Map<String, ? extends OIDCServer> servers) {
this.servers = servers;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "OIDCServers [servers=" + servers
+ "]";
}
}

View File

@ -35,26 +35,17 @@
<!-- End view configuration -->
<bean class="org.mitre.account_chooser.OIDCServers">
<property name="servers">
<bean name="AccountChooserConfig" class="org.mitre.account_chooser.AccountChooserConfig">
<property name="issuers">
<map>
<entry key="1">
<entry key="http://server.example.com:8080/openid-connect-server">
<bean class="org.mitre.account_chooser.OIDCServer">
<property name="name" value="OIDC Server 1" />
</bean>
</entry>
<entry key="2">
<bean class="org.mitre.account_chooser.OIDCServer">
<property name="name" value="OIDC Server 2" />
</bean>
</entry>
<entry key="3">
<bean class="org.mitre.account_chooser.OIDCServer">
<property name="name" value="OIDC Server 3" />
<property name="name" value="Example Server" />
</bean>
</entry>
</map>
</property>
<property name="validClientIds" value="FGWEUIASJK, IUYTTYEV, GFHDSFYD" />
</bean>
</beans>

View File

@ -3,9 +3,8 @@
<%@page import="org.mitre.account_chooser.OIDCServer"%>
<%@page import="java.util.Map"%>
<%@page import="java.util.Iterator"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<jsp:useBean id="servers" type="org.mitre.account_chooser.OIDCServers"
<jsp:useBean id="issuers" type="java.util.Map"
scope="request" />
<!DOCTYPE html>
@ -36,22 +35,21 @@ body {
<div class="container">
<div class="row">
<div class="span12">
<form class="form-horizontal" action="selected" method="get">
<form name="chooser" class="form-horizontal" action="selected" method="get">
<div class="control-group">
<label class="control-label" for="select01">Account:</label>
<div class="controls">
<select name="alias">
<select name="issuer">
<%
Map map = servers.getServers();
Iterator entries = map.entrySet().iterator();
Iterator entries = issuers.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry entry = (Map.Entry) entries.next();
String alias = (String) entry.getKey();
OIDCServer server = (OIDCServer) entry.getValue();
String id = (String) entry.getKey();
OIDCServer issuer = (OIDCServer) entry.getValue();
%>
<option value="<%= alias %>"><%= server.getName() %></option>
<option value="<%= id %>"><%= issuer.getName() %></option>
<%
}
%>
@ -61,12 +59,13 @@ body {
</div>
<div class="control-group">
<div class="controls">
<input name="redirect_uri" type="hidden" value="<c:out value="${redirect_uri}"/>">
<input name="redirect_uri" type="hidden" value="${redirect_uri}">
<input name="client_id" type="hidden" value="${client_id}">
</div>
</div>
<div class="form-actions">
<button class="btn btn-primary" type="submit">Submit</button>
<button class="btn">Cancel</button>
<a class="btn btn-primary" href="javascript: submitForm('selected')">Submit</a>
<a class="btn" href="javascript: submitForm('cancel')">Cancel</a>
</div>
</form>
</div>
@ -94,7 +93,10 @@ body {
<script type="text/javascript" language="JavaScript">
// <![CDATA [
// javascript secific to the page would go here, if I had any...
function submitForm(action){
$('.form-horizontal').attr( 'action' , action );
$('.form-horizontal').submit();
}
// ]]>
</script>

View File

@ -0,0 +1,70 @@
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@page import="org.mitre.account_chooser.OIDCServer"%>
<%@page import="java.util.Map"%>
<%@page import="java.util.Iterator"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Account Chooser</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Account Chooser GUI">
<meta name="author" content="nemonik">
<link href="resources/bootstrap/css/bootstrap.css" rel="stylesheet">
<link href="resources/bootstrap/css/bootstrap-responsive.css"
rel="stylesheet">
<link href="resources/bootstrap/css/docs.css" rel="stylesheet">
<style>
body {
padding-top: 60px;
/* 60px to make the container go all the way to the bottom of the topbar */
}
</style>
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<div class="row">
<div class="span12">
<h1>
${error}<br>
<small>${error_description}</small>
</h1>
<a href="${client_uri}">Return to client</a>
</div>
</div>
</div>
<!--/container-->
<!-- Placed at the end of the document so the pages load faster -->
<script src="resources/bootstrap/js/jquery.js"></script>
<script src="resources/bootstrap/js/bootstrap-transition.js"></script>
<script src="resources/bootstrap/js/bootstrap-alert.js"></script>
<script src="resources/bootstrap/js/bootstrap-modal.js"></script>
<script src="resources/bootstrap/js/bootstrap-dropdown.js"></script>
<script src="resources/bootstrap/js/bootstrap-scrollspy.js"></script>
<script src="resources/bootstrap/js/bootstrap-tab.js"></script>
<script src="resources/bootstrap/js/bootstrap-tooltip.js"></script>
<script src="resources/bootstrap/js/bootstrap-popover.js"></script>
<script src="resources/bootstrap/js/bootstrap-button.js"></script>
<script src="resources/bootstrap/js/bootstrap-collapse.js"></script>
<script src="resources/bootstrap/js/bootstrap-carousel.js"></script>
<script src="resources/bootstrap/js/bootstrap-typeahead.js"></script>
<script type="text/javascript" language="JavaScript">
// <![CDATA [
// ]]>
</script>
</body>
</html>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="org.eclipse.jst.component.nondependency" value=""/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="org.eclipse.jst.component.nondependency" value=""/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -1,36 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>openid-connect-client</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>
</projectDescription>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>openid-connect-client</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>
</projectDescription>

View File

@ -2,11 +2,11 @@
## Overview
This is the Client, a Spring Security AuthenticationFilter, to OpenID Connect Java Spring Server described by [OpenID Connect Standard].
This is the Client, a Spring Security AuthenticationFilter, to the OpenID Connect Java Spring Server following the [OpenID Connect Standard] described protocol.
## Configure
## Configuration of OIDCAuthenticationFilter
Configure the OpenIDConnectAuthenticationFilter by adding the XML to your application context security like so:
Configure the OIDCAuthenticationFilter by adding the XML to your application context security like so making changes where necessary for your deployment:
<security:http auto-config="false"
use-expressions="true"
@ -40,7 +40,7 @@ Configure the OpenIDConnectAuthenticationFilter by adding the XML to your applic
<security:authentication-manager alias="authenticationManager" />
<bean id="openIdConnectAuthenticationProvider"
class='org.mitre.openid.connect.client.OpenIdConnectAuthenticationProvider">
class='org.mitre.openid.connect.client.OIDCAuthenticationProvider">
<property name="userDetaulsService" ref="myUserDetailsService"/>
</bean>
@ -63,111 +63,22 @@ Configure the OpenIDConnectAuthenticationFilter by adding the XML to your applic
You will need to implement your own UserDetailsService and configure as the above does with the reference to *myUserDetailsService*.
## Proposed Account Chooser UI Application Extension
## Configuration of OIDCAuthenticationUsingChooserFilter
The following proposed extension is in response to [Issue #39].
The OIDCAuthenticationUsingChooserFilter was written in response to [Issue #39].
### Account Chooser Protocol
The following describes the protocol between the Client and Account Chooser UI application introduced in [Issue #39].
#### Authorization when using Account Chooser Code Flow
The Authorization when using Account Chooser Code Flow goes through the following steps.
1. Client prepares an Account Chooser Request containing the desired request parameters.
2. Client sends a request to the Account Chooser.
3. Account Chooser presents a selection of OpenID Connect (OIDC) Servers from which the End-User must select from.
4. End-User selects an OIDC.
5. Account Chooser Sends the End-User back to the Client with key value of the OIDC End-User selected.
6. The Client begins the Authorization flow desrcribed in [Authorization Code Flow][OpenID Connect Standard] of the [OpenID Connect Standard].
#### Account Chooser Request
When the End-User wishes to access a Protected Resource, and the End-User Authorization has not yet been obtained, the Client will redirect the End-User to Account Chooser.
Account Chooser MUST support the use of the HTTP "GET" and "POST" methods defined in RFC 2616 [RFC2616].
Clients MAY use the HTTP "GET" or "POST" method to send the Account Chooser Request to the Account Chooser. If using the HTTP "GET" method, the request parameters are serialized using URI query string serialization. If using the HTTP "POST" method, the request parameters are serialized using form serialization.
#### Client Prepares an Account Chooser Request
The Client prepares an Account Chooser Request to the Account Chooser with the request parameters using the HTTP "GET" or "POST" method.
The required Account Chooser Request parameters are as follows:
* redirect_uri - A redirection URI where the response will be sent.
There is one method to construct and send the request to the Account Chooser:
a. Simple Request Method
#### Simple Request Method
The Client prepares an Account Chooser Request to the Account Chooser using the appropriate parameters. If using the HTTP "GET" method, the request parameters are serialized using URI query string serialization. If using the HTTP "POST" method, the request parameters are serialized using form serialization.
The following is a non-normative example of an Account Chooser Request URL. Line wraps are for display purposes only.
http://server.example.com/chooser?
redirect_uri=https%3A%2F%2Fclient.example.com%2Fopenid_connect_login
#### Client sends a request to the Account Chooser
Having constructed the Account Chooser Request, the Client sends it to the Account Chooser. This MAY happen via redirect, hyperlinking, or any other means of directing the User-Agent to the Account Chooser URL.
Following is a non-normative example using HTTP redirect. Line wraps are for display purposes only.
HTTP/1.1 302 Found
Location: https://server.example.com/chooser?
redirect_uri=https%3A%2F%2Fclient.example.com%2Fopenid_connect_login
#### Account Chooser Sends the End-User back to the Client
After the End-User has select an OpenID Connect Server, it issues an Account Chooser Response and delivers it to the Client by adding the response parameters to redirect_uri specified in the Account Choose Request using the "application/x-www-form-urlencoded" format.
The following response parameters are included:
* oidc_alias - REQUIRED. The key used to configure the Client for its request of the selected OIDC server.
The following is non-normative example of a responses. Line wraps are for display purposes only.
HTTP/1.1 302 Found
Location: https://client.example.com/openid_connect_login?
oidc_alias=OIDC%20Server%201
#### End-User refuses to select an OIDC Server
If the End-User refuses to select an OIDC server, the Account Chooser MUST return an error response. The Account Chooser returns the Client via the redirection URI specified in the Account Chooser Request with the appropriate error parameters. No other parameters SHOULD be returned.
The error response parameters are the following:
* error - REQUIRED. The error code.
* error_description - OPTIONAL. A human-readable UTF-8 encoded text description of the error.
The response parameters are added to the query component of the redirection URI.
The following is a non-normative example. Line wraps after the second line are for the display purposes only.
HTTP/1.1 302 Found
Location: https://client.example.com/openid_connect_login?
error=end_user_cancelled
&error_description=The%20end%20user%20refused%20to%20select%20an%20OIDC%20server
### Modification to existing Client
#### Modifications to Client Configuration
The configuration of the filter would change by adding a OIDCServers property to the Client containing a map of OIDC servers, and a AccountChooserURI to denote the URI of the Account Chooser like so:
The Authentication Filter uses the *oidcServerConfigs* property, a map of OIDC servers; an *accountChooserURI* property to denote the URI of the Account Chooser; and an *accountChooserClient* property to identify the Client to the Account Chooser UI application like so with modifications specific to your deployment:
<bean id="openIdConnectAuthenticationFilter"
class="org.mitre.openid.connect.client.OpenIdConnectAuthenticationFilter">
class="org.mitre.openid.connect.client.OIDCAuthenticationUsingChooserFilter">
<property name="errorRedirectURI" value="/login.jsp?authfail=openid" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="AccountChooserURI"
<property name="accountChooserURI"
value="http://sever.example.com:8080/account-chooser" />
<property name="accountChooserClientID" value="FGWEUIASJK" />
<property name="oidcServerConfigs">
<map>
<entry key="OIDC Server 1">
<entry key="http://sever.example.com:8080/Fopenid-connect-server">
<bean class="org.mitre.openid.connect.client.OIDCServerConfiguration">
<property name="authorizationEndpointURI"
value="http://sever.example.com:8080/openid-connect-server/oauth/authorize" />
@ -180,17 +91,103 @@ The configuration of the filter would change by adding a OIDCServers property to
<property name="clientSecret" value="someClientSecret" />
</bean>
</entry>
<entry key="OIDC Server 2">
...
<entry key=". . .
</map>
</property>
</bean>
Again, you will need to implement your own UserDetailsService and configure as the above does with the reference to *myUserDetailsService*.
## Implementing your own UserDetailsService
An example UserDetailsService for the Rave Portal follows:
package org.mitre.mpn.service.impl;
import org.apache.rave.portal.model.NewUser;
import org.apache.rave.portal.model.User;
import org.apache.rave.portal.service.NewAccountService;
import org.apache.rave.portal.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.mitre.openid.connect.client.OpenIdConnectAuthenticationToken;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service(value = "myUserDetailsService")
public class MyUserDetailsService implements UserDetailsService,
AuthenticationUserDetailsService<OpenIdConnectAuthenticationToken> {
private static final Logger log = LoggerFactory.getLogger(MpnUserDetailsService.class);
private final UserService userService;
private final NewAccountService newAccountService;
//TODO: This is temporarily hard-coded while we wait for the concept of Page Templates to be implemented in Rave
private static final String DEFAULT_LAYOUT_CODE = "columns_3";
@Autowired
public MyUserDetailsService(UserService userService, NewAccountService newAccountService) {
this.userService = userService;
this.newAccountService = newAccountService;
}
/* (non-Javadoc)
* @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String)
*/
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.debug("loadUserByUsername called with: {}", username);
User user = userService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User with username '" + username + "' was not found!");
}
return user;
}
/* (non-Javadoc)
* @see org.springframework.security.core.userdetails.AuthenticationUserDetailsService#loadUserDetails(org.springframework.security.core.Authentication)
*/
public UserDetails loadUserDetails(OpenIdConnectAuthenticationToken token) throws UsernameNotFoundException {
log.debug("loadUserDetails called with: {}", token);
User user = userService.getUserByUsername(token.getUserId());
if (user == null) {
NewUser newUser = new NewUser();
newUser.setUsername(token.getUserId());
newUser.setEmail(token.getUserId() + "@example.com");
newUser.setPageLayout(DEFAULT_LAYOUT_CODE);
newUser.setPassword(UUID.randomUUID().toString());
try {
newAccountService.createNewAccount(newUser);
} catch (Exception e) {
throw new RuntimeException(e);
}
user = userService.getUserByUsername(token.getName());
}
return user;
}
}
In cases where the Account Chooser will not be used, the Client will be configured with authorizationEndpointURI, tokenEndpointURI, checkIDEndpointURI, clientId, and clientSecret as the Client is presently.
[OpenID Connect Standard]: http://openid.net/specs/openid-connect-standard-1_0.html "OpenID Connect Standard 1.0"
[OpenID Connect Standard]: http://openid.net/specs/openid-connect-standard-1_0.html#code_flow "Authorization Code Flow, OpenID Connect Standard"
[Issue #39]: http://github.com/jricher/OpenID-Connect-Java-Spring-Server/issues/39 "Issue #39 -- Multiple Point Client"
[Issuer Identifier]: http://openid.net/specs/openid-connect-messages-1_0.html#issuer_identifier "Issuer Identifier"
[Issue #39]: http://github.com/jricher/OpenID-Connect-Java-Spring-Server/issues/39 "Issue #39 -- Multiple Point Client"

View File

@ -26,7 +26,9 @@ import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
@ -35,6 +37,7 @@ import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64;
@ -67,16 +70,73 @@ import com.google.gson.JsonParser;
* @author nemonik
*
*/
public class OpenIdConnectAuthenticationFilter extends
public class AbstractOIDCAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
private final static int HTTP_SOCKET_TIMEOUT = 30000;
private final static String SCOPE = "openid";
private final static int KEY_SIZE = 1024;
private final static String SIGNING_ALGORITHM = "SHA256withRSA";
private final static String NONCE_SIGNATURE_COOKIE_NAME = "nonce";
private final static String OIDC_ALIAS_COOKIE_NAME = "oidc_alias";
private final static String FILTER_PROCESSES_URL = "/openid_connect_login";
/**
* Used to remove parameters from a Request before passing it down the chain...
*
* @author nemonik
*
*/
class SanatizedRequest extends HttpServletRequestWrapper {
private List<String> paramsToBeSanatized;
public SanatizedRequest(HttpServletRequest request,
String[] paramsToBeSanatized) {
super(request);
this.paramsToBeSanatized = Arrays.asList(paramsToBeSanatized);
}
public String getParameter(String name) {
if (paramsToBeSanatized.contains(name)) {
return null;
} else {
return super.getParameter(name);
}
}
public Map<String, String[]> getParameterMap() {
Map<String, String[]> params = super.getParameterMap();
for (String paramToBeSanatized : paramsToBeSanatized) {
params.remove(paramToBeSanatized);
}
return params;
}
public Enumeration<String> getParameterNames() {
ArrayList<String> paramNames = Collections.list(super
.getParameterNames());
for (String paramToBeSanatized : paramsToBeSanatized) {
paramNames.remove(paramToBeSanatized);
}
return Collections.enumeration(paramNames);
}
public String[] getParameterValues(String name) {
if (paramsToBeSanatized.contains(name)) {
return null;
} else {
return super.getParameterValues(name);
}
}
}
protected final static int HTTP_SOCKET_TIMEOUT = 30000;
protected final static String SCOPE = "openid";
protected final static int KEY_SIZE = 1024;
protected final static String SIGNING_ALGORITHM = "SHA256withRSA";
protected final static String NONCE_SIGNATURE_COOKIE_NAME = "nonce";
protected final static String FILTER_PROCESSES_URL = "/openid_connect_login";
/**
* Builds the redirect_uri that will be sent to the Authorization Endpoint.
@ -105,7 +165,9 @@ public class OpenIdConnectAuthenticationFilter extends
String name = (String) e.nextElement();
if ((ignore == null) || (!ignore.contains(name))) {
// Assume for simplicity that there is only one value
String value = request.getParameter(name);
if (value == null) {
@ -150,13 +212,19 @@ public class OpenIdConnectAuthenticationFilter extends
char appendChar = '?';
for (Map.Entry<String, String> param : queryStringFields.entrySet()) {
try {
URLBuilder.append(appendChar).append(param.getKey())
.append('=')
.append(URLEncoder.encode(param.getValue(), "UTF-8"));
} catch (UnsupportedEncodingException uee) {
throw new IllegalStateException(uee);
}
appendChar = '&';
}
@ -188,8 +256,11 @@ public class OpenIdConnectAuthenticationFilter extends
.replace("=", "");
} catch (GeneralSecurityException generalSecurityException) {
// generalSecurityException.printStackTrace();
throw new IllegalStateException(generalSecurityException);
}
return signature;
@ -216,39 +287,37 @@ public class OpenIdConnectAuthenticationFilter extends
return signer.verify(sigBytes);
} catch (GeneralSecurityException generalSecurityException) {
// generalSecurityException.printStackTrace();
throw new IllegalStateException(generalSecurityException);
} catch (UnsupportedEncodingException unsupportedEncodingException) {
// unsupportedEncodingException.printStackTrace();
throw new IllegalStateException(unsupportedEncodingException);
}
}
private String errorRedirectURI;
protected String errorRedirectURI;
private OIDCServerConfiguration oidcServerConfig;
protected String scope;
private String accountChooserURI;
protected int httpSocketTimeout = HTTP_SOCKET_TIMEOUT;
private Map<String, ? extends OIDCServerConfiguration> oidcServerConfigs = new HashMap<String, OIDCServerConfiguration>();
protected PublicKey publicKey;
private String scope;
protected PrivateKey privateKey;
private int httpSocketTimeout = HTTP_SOCKET_TIMEOUT;
private PublicKey publicKey;
private PrivateKey privateKey;
private Signature signer;
protected Signature signer;
/**
* OpenIdConnectAuthenticationFilter constructor
*/
protected OpenIdConnectAuthenticationFilter() {
protected AbstractOIDCAuthenticationFilter() {
super(FILTER_PROCESSES_URL);
oidcServerConfig = new OIDCServerConfiguration();
}
@Override
@ -258,40 +327,6 @@ public class OpenIdConnectAuthenticationFilter extends
Assert.notNull(errorRedirectURI,
"An Error Redirect URI must be supplied");
try {
// Validating configuration w/ Account Chooser UI Application
// settings
Assert.notNull(
oidcServerConfigs,
"Server Configurations must be supplied if the Account Chooser UI Application is to be used.");
Assert.notNull(
accountChooserURI,
"Account Chooser URI must be supplied if the Account Chooser UI Application is to be used.");
} catch (Exception e) {
// Failing over to validating configuration w/o Account Chooser UI
// Application settings
Assert.notNull(oidcServerConfig.getAuthorizationEndpointURI(),
"An Authorization Endpoint URI must be supplied");
Assert.notNull(oidcServerConfig.getTokenEndpointURI(),
"A Token ID Endpoint URI must be supplied");
Assert.notNull(oidcServerConfig.getCheckIDEndpointURI(),
"A Check ID Endpoint URI must be supplied");
Assert.notNull(oidcServerConfig.getClientId(),
"A Client ID must be supplied");
Assert.notNull(oidcServerConfig.getClientSecret(),
"A Client Secret must be supplied");
}
KeyPairGenerator keyPairGenerator;
try {
@ -325,96 +360,29 @@ public class OpenIdConnectAuthenticationFilter extends
HttpServletResponse response) throws AuthenticationException,
IOException, ServletException {
// Enter AuthenticationFilter here...
if (StringUtils.isNotBlank(request.getParameter("error"))) {
handleError(request, response);
} else if (request.getParameter("code") != null) {
return handleAuthorizationGrantResponse(request);
} else if (StringUtils.isNotBlank(accountChooserURI)) {
String oidcAlias = request.getParameter("oidc_alias");
if (StringUtils.isNotBlank(oidcAlias)) {
// Account Chooser UI selects the appropriate OIDC Server configuration
OIDCServerConfiguration oidcServerConfig = oidcServerConfigs
.get(oidcAlias);
if (oidcServerConfig != null) {
Cookie oidcAliasCookie = new Cookie(OIDC_ALIAS_COOKIE_NAME, oidcAlias);
response.addCookie(oidcAliasCookie);
handleAuthorizationRequest(request, response, oidcServerConfig);
} else {
throw new AuthenticationServiceException(
"Security Filter not configured for OIDC Alias: "
+ oidcAlias);
}
} else {
// redirect the End-User to the configured Account Chooser UI
Map<String, String> urlVariables = new HashMap<String, String>();
urlVariables.put("redirect_uri", OpenIdConnectAuthenticationFilter
.buildRedirectURI(request, null));
response.sendRedirect(OpenIdConnectAuthenticationFilter.buildURL(
accountChooserURI, urlVariables));
}
} else {
// Use configuration that doesn't involve the Account Chooser UI.
handleAuthorizationRequest(request, response, this.oidcServerConfig);
}
logger.debug("Request: " + request.getRequestURI() + (StringUtils.isNotBlank(request.getQueryString()) ? "?"
+ request.getQueryString() : "") );
return null;
}
public String getAccountChooserURI() {
return accountChooserURI;
}
public Map<String, ? extends OIDCServerConfiguration> getOidcServerConfigs() {
return oidcServerConfigs;
}
/**
* Handles the authorization grant response
*
* @param authorizationGrant
* The Authorization grant code
* @param request
* The request from which to extract parameters and perform the
* authentication
* @return The authenticated user token, or null if authentication is
* incomplete.
* @throws UnsupportedEncodingException
*/
private Authentication handleAuthorizationGrantResponse(
HttpServletRequest request) {
protected Authentication handleAuthorizationGrantResponse(
String authorizationGrant, HttpServletRequest request,
OIDCServerConfiguration serverConfig) {
final boolean debug = logger.isDebugEnabled();
OIDCServerConfiguration serverConfig = this.oidcServerConfig;
// Which OIDC configuration?
Cookie oidcAliasCookie = WebUtils.getCookie(request, OIDC_ALIAS_COOKIE_NAME);
if (oidcAliasCookie != null) {
serverConfig = oidcServerConfigs.get(oidcAliasCookie.getValue());
}
String authorizationGrant = request.getParameter("code");
// Handle Token Endpoint interaction
HttpClient httpClient = new DefaultHttpClient();
@ -426,11 +394,11 @@ public class OpenIdConnectAuthenticationFilter extends
// TODO: basic auth is untested (it wasn't working last I
// tested)
// UsernamePasswordCredentials credentials = new
// UsernamePasswordCredentials(
// clientId, clientSecret);
// ((DefaultHttpClient) httpClient).getCredentialsProvider()
// .setCredentials(AuthScope.ANY, credentials);
//
// UsernamePasswordCredentials(serverConfig.getClientId(),
// serverConfig.getClientSecret());
// ((DefaultHttpClient)
// httpClient).getCredentialsProvider().setCredentials(AuthScope.ANY,
// credentials);
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(
httpClient);
@ -440,23 +408,24 @@ public class OpenIdConnectAuthenticationFilter extends
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
form.add("grant_type", "authorization_code");
form.add("code", authorizationGrant);
form.add("redirect_uri", OpenIdConnectAuthenticationFilter
.buildRedirectURI(request, new String[] { "code" }));
form.add("redirect_uri", AbstractOIDCAuthenticationFilter
.buildRedirectURI(request, null));
// pass clientId and clientSecret in post of request
form.add("client_id", serverConfig.getClientId());
form.add("client_secret", serverConfig.getClientSecret());
if (debug) {
logger.debug("tokenEndpointURI = " + serverConfig.getTokenEndpointURI());
logger.debug("tokenEndpointURI = "
+ serverConfig.getTokenEndpointURI());
logger.debug("form = " + form);
}
String jsonString = null;
try {
jsonString = restTemplate.postForObject(serverConfig.getTokenEndpointURI(), form,
String.class);
jsonString = restTemplate.postForObject(
serverConfig.getTokenEndpointURI(), form, String.class);
} catch (HttpClientErrorException httpClientErrorException) {
// Handle error
@ -554,8 +523,9 @@ public class OpenIdConnectAuthenticationFilter extends
jsonString = null;
try {
jsonString = restTemplate.postForObject(serverConfig.getCheckIDEndpointURI(),
form, String.class);
jsonString = restTemplate.postForObject(
serverConfig.getCheckIDEndpointURI(), form,
String.class);
} catch (HttpClientErrorException httpClientErrorException) {
// Handle error
@ -651,7 +621,7 @@ public class OpenIdConnectAuthenticationFilter extends
* @throws IOException
* If an input or output exception occurs
*/
private void handleAuthorizationRequest(HttpServletRequest request,
protected void handleAuthorizationRequest(HttpServletRequest request,
HttpServletResponse response,
OIDCServerConfiguration serverConfiguration) throws IOException {
@ -662,7 +632,7 @@ public class OpenIdConnectAuthenticationFilter extends
urlVariables.put("response_type", "code");
urlVariables.put("client_id", serverConfiguration.getClientId());
urlVariables.put("scope", scope);
urlVariables.put("redirect_uri", OpenIdConnectAuthenticationFilter
urlVariables.put("redirect_uri", AbstractOIDCAuthenticationFilter
.buildRedirectURI(request, null));
// Create a string value used to associate a user agent session
@ -684,8 +654,13 @@ public class OpenIdConnectAuthenticationFilter extends
// TODO: display, prompt, request, request_uri
response.sendRedirect(OpenIdConnectAuthenticationFilter.buildURL(
serverConfiguration.getAuthorizationEndpointURI(), urlVariables));
String authRequest = AbstractOIDCAuthenticationFilter
.buildURL(serverConfiguration.getAuthorizationEndpointURI(),
urlVariables);
logger.debug("Auth Request: " + authRequest);
response.sendRedirect(authRequest);
}
/**
@ -699,7 +674,7 @@ public class OpenIdConnectAuthenticationFilter extends
* @throws IOException
* If an input or output exception occurs
*/
private void handleError(HttpServletRequest request,
protected void handleError(HttpServletRequest request,
HttpServletResponse response) throws IOException {
String error = request.getParameter("error");
@ -718,44 +693,15 @@ public class OpenIdConnectAuthenticationFilter extends
requestParams.put("error_uri", errorURI);
}
response.sendRedirect(OpenIdConnectAuthenticationFilter.buildURL(
response.sendRedirect(AbstractOIDCAuthenticationFilter.buildURL(
errorRedirectURI, requestParams));
}
public void setAccountChooserURI(String accountChooserURI) {
this.accountChooserURI = accountChooserURI;
}
public void setAuthorizationEndpointURI(String authorizationEndpointURI) {
oidcServerConfig.setAuthorizationEndpointURI(authorizationEndpointURI);
}
public void setCheckIDEndpointURI(String checkIDEndpointURI) {
oidcServerConfig.setCheckIDEndpointURI(checkIDEndpointURI);
}
public void setClientId(String clientId) {
oidcServerConfig.setClientId(clientId);
}
public void setClientSecret(String clientSecret) {
oidcServerConfig.setClientSecret(clientSecret);
}
public void setErrorRedirectURI(String errorRedirectURI) {
this.errorRedirectURI = errorRedirectURI;
}
public void setOidcServerConfigs(
Map<String, ? extends OIDCServerConfiguration> oidcServerConfigs) {
this.oidcServerConfigs = oidcServerConfigs;
}
public void setScope(String scope) {
this.scope = scope;
}
public void setTokenEndpointURI(String tokenEndpointURI) {
oidcServerConfig.setTokenEndpointURI(tokenEndpointURI);
}
}

View File

@ -0,0 +1,135 @@
/*******************************************************************************
* Copyright 2012 The MITRE Corporation
*
* 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.mitre.openid.connect.client;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.Assert;
/**
* The OpenID Connect Authentication Filter
*
* See README.md to to configure
*
* @author nemonik
*
*/
public class OIDCAuthenticationFilter extends AbstractOIDCAuthenticationFilter {
protected OIDCServerConfiguration oidcServerConfig;
/**
* OpenIdConnectAuthenticationFilter constructor
*/
protected OIDCAuthenticationFilter() {
super();
oidcServerConfig = new OIDCServerConfiguration();
}
/*
* (non-Javadoc)
*
* @see org.mitre.openid.connect.client.AbstractOIDCAuthenticationFilter#
* afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
// Validating configuration
Assert.notNull(oidcServerConfig.getAuthorizationEndpointURI(),
"An Authorization Endpoint URI must be supplied");
Assert.notNull(oidcServerConfig.getTokenEndpointURI(),
"A Token ID Endpoint URI must be supplied");
Assert.notNull(oidcServerConfig.getCheckIDEndpointURI(),
"A Check ID Endpoint URI must be supplied");
Assert.notNull(oidcServerConfig.getClientId(),
"A Client ID must be supplied");
Assert.notNull(oidcServerConfig.getClientSecret(),
"A Client Secret must be supplied");
}
/*
* (non-Javadoc)
*
* @see org.mitre.openid.connect.client.AbstractOIDCAuthenticationFilter#
* attemptAuthentication(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException,
IOException, ServletException {
// Enter AuthenticationFilter here...
super.attemptAuthentication(request, response);
if (StringUtils.isNotBlank(request.getParameter("error"))) {
handleError(request, response);
} else if (StringUtils.isNotBlank(request.getParameter("code"))) {
return handleAuthorizationGrantResponse(
request.getParameter("code"), new SanatizedRequest(request,
new String[] { "code" }), oidcServerConfig);
} else {
handleAuthorizationRequest(request, response, oidcServerConfig);
}
return null;
}
public void setAuthorizationEndpointURI(String authorizationEndpointURI) {
oidcServerConfig.setAuthorizationEndpointURI(authorizationEndpointURI);
}
public void setCheckIDEndpointURI(String checkIDEndpointURI) {
oidcServerConfig.setCheckIDEndpointURI(checkIDEndpointURI);
}
public void setClientId(String clientId) {
oidcServerConfig.setClientId(clientId);
}
public void setClientSecret(String clientSecret) {
oidcServerConfig.setClientSecret(clientSecret);
}
public void setErrorRedirectURI(String errorRedirectURI) {
this.errorRedirectURI = errorRedirectURI;
}
public void setTokenEndpointURI(String tokenEndpointURI) {
oidcServerConfig.setTokenEndpointURI(tokenEndpointURI);
}
}

View File

@ -0,0 +1,183 @@
/*******************************************************************************
* Copyright 2012 The MITRE Corporation
*
* 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.mitre.openid.connect.client;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.Assert;
import org.springframework.web.util.WebUtils;
/**
* The OpenID Connect Authentication Filter using Acount Chooser UI Application
*
* See README.md to to configure
*
* @author nemonik
*
*/
public class OIDCAuthenticationUsingChooserFilter extends
AbstractOIDCAuthenticationFilter {
protected final static String ISSUER_COOKIE_NAME = "issuer";
protected String accountChooserURI;
protected String accountChooserClientID;
protected Map<String, ? extends OIDCServerConfiguration> oidcServerConfigs = new HashMap<String, OIDCServerConfiguration>();
/**
* OpenIdConnectAuthenticationFilter constructor
*/
protected OIDCAuthenticationUsingChooserFilter() {
super();
}
/*
* (non-Javadoc)
*
* @see org.mitre.openid.connect.client.AbstractOIDCAuthenticationFilter#
* afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
// Validating configuration
Assert.notNull(
oidcServerConfigs,
"Server Configurations must be supplied if the Account Chooser UI Application is to be used.");
Assert.notNull(
accountChooserURI,
"Account Chooser URI must be supplied if the Account Chooser UI Application is to be used.");
Assert.notNull(
accountChooserClientID,
"Account Chooser Client ID must be supplied if the Account Chooser UI Application is to be used.");
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.web.authentication.
* AbstractAuthenticationProcessingFilter
* #attemptAuthentication(javax.servlet.http.HttpServletRequest,
* javax.servlet.http.HttpServletResponse)
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws IOException,
AuthenticationException, ServletException {
// Enter AuthenticationFilter here...
super.attemptAuthentication(request, response);
if (StringUtils.isNotBlank(request.getParameter("error"))) {
handleError(request, response);
} else if (request.getParameter("code") != null) {
// Which OIDC configuration?
Cookie issuerCookie = WebUtils.getCookie(request,
ISSUER_COOKIE_NAME);
return handleAuthorizationGrantResponse(
request.getParameter("code"), new SanatizedRequest(request,
new String[] { "code" }),
oidcServerConfigs.get(issuerCookie.getValue()));
} else {
String issuer = request.getParameter("issuer");
if (StringUtils.isNotBlank(issuer)) {
// Account Chooser UI provided and Issuer Identifier
OIDCServerConfiguration oidcServerConfig = oidcServerConfigs
.get(issuer);
if (oidcServerConfig != null) {
// The Client is configured to support this Issuer
// Identifier
Cookie issuerCookie = new Cookie(ISSUER_COOKIE_NAME,
issuer);
response.addCookie(issuerCookie);
handleAuthorizationRequest(new SanatizedRequest(request,
new String[] { "issuer" }), response,
oidcServerConfig);
} else {
// The Client is NOT configured to support this Issuer
// Identifier
throw new AuthenticationServiceException(
"Security Filter not configured for issuer: "
+ issuer);
}
} else {
// Redirect the End-User to the configured Account Chooser UI
// application
Map<String, String> urlVariables = new HashMap<String, String>();
urlVariables.put("redirect_uri",
OIDCAuthenticationUsingChooserFilter.buildRedirectURI(
request, null));
urlVariables.put("client_id", accountChooserClientID);
response.sendRedirect(OIDCAuthenticationUsingChooserFilter
.buildURL(accountChooserURI, urlVariables));
}
}
return null;
}
public void setAccountChooserClientID(String accountChooserClientID) {
this.accountChooserClientID = accountChooserClientID;
}
public void setAccountChooserURI(String accountChooserURI) {
this.accountChooserURI = accountChooserURI;
}
public void setOidcServerConfigs(
Map<String, ? extends OIDCServerConfiguration> oidcServerConfigs) {
this.oidcServerConfigs = oidcServerConfigs;
}
}

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="org.eclipse.jst.component.nondependency" value=""/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="org.eclipse.jst.component.nondependency" value=""/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -19,18 +19,10 @@
package org.mitre.oauth2.model;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Basic;
import javax.persistence.CollectionTable;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import javax.persistence.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.provider.ClientDetails;
@ -55,23 +47,23 @@ public class ClientDetailsEntity implements ClientDetails {
public enum AuthType {
client_secret_post, client_secret_basic, client_secret_jwt, private_key_jwt
};
private String clientId;
private String clientSecret;
private Set<String> scope;
private Set<String> authorizedGrantTypes;
private Set<GrantedAuthority> authorities = Collections.emptySet();
private String clientName;
private String clientDescription;
private boolean allowRefresh = false; // do we allow refresh tokens for this client?
private Integer accessTokenTimeout; // in seconds
private Integer refreshTokenTimeout; // in seconds
private String owner; // userid of who registered it
private Set<String> registeredRedirectUri;
private Set<String> resourceIds;
}
//Additional properties added by OpenID Connect Dynamic Client Registration spec
private String clientId = "";
private String clientSecret = "";
private Set<String> scope = new HashSet<String>();
private Set<String> authorizedGrantTypes = new HashSet<String>();
private Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
private String clientName = "";
private String clientDescription = "";
private boolean allowRefresh = false; // do we allow refresh tokens for this client?
private Integer accessTokenTimeout = 0; // in seconds
private Integer refreshTokenTimeout = 0; // in seconds
private String owner = ""; // userid of who registered it
private Set<String> registeredRedirectUri = new HashSet<String>();
private Set<String> resourceIds = new HashSet<String>();
//Additional properties added by OpenID Connect Dynamic Client Registration spec
//http://openid.net/specs/openid-connect-registration-1_0.html
/**
@ -122,7 +114,7 @@ public class ClientDetailsEntity implements ClientDetails {
/**
* @return the clientId
*/
@Id
@Id @GeneratedValue
public String getClientId() {
return clientId;
}

View File

@ -28,10 +28,14 @@ public interface ClientDetailsEntityService extends ClientDetailsService {
public ClientDetailsEntity loadClientByClientId(String clientId) throws OAuth2Exception;
public ClientDetailsEntity createClient(String clientId, String clientSecret, Set<String> scope, Set<String> grantTypes, String redirectUri, Set<GrantedAuthority> authorities, Set<String> resourceIds, String name, String description, boolean allowRefresh, Integer accessTokenTimeout, Integer refreshTokenTimeout, String owner);
public ClientDetailsEntity createClient(ClientDetailsEntity client);
public void deleteClient(ClientDetailsEntity client);
public ClientDetailsEntity updateClient(ClientDetailsEntity oldClient, ClientDetailsEntity newClient);
public ClientDetailsEntity saveClient(ClientDetailsEntity client);
public Collection<ClientDetailsEntity> getAllClients();
}

View File

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources"/>
<classpathentry kind="src" output="target/test-classes" path="src/test/java"/>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources"/>
<classpathentry kind="src" output="target/test-classes" path="src/test/java"/>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -1,43 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>openid</name>
<comment>Reference implementation of OpenID Connect spec (http://openid.net/connect/). NO_M2ECLIPSE_SUPPORT: Project files created with the maven-eclipse-plugin are not supported in M2Eclipse.</comment>
<projects>
<project>spring-security-oauth2</project>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.wst.jsdt.core.javascriptValidator</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>openid</name>
<comment>Reference implementation of OpenID Connect spec (http://openid.net/connect/). NO_M2ECLIPSE_SUPPORT: Project files created with the maven-eclipse-plugin are not supported in M2Eclipse.</comment>
<projects>
<project>spring-security-oauth2</project>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.wst.jsdt.core.javascriptValidator</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -5,10 +5,7 @@
<wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resources"/>
<wb-resource deploy-path="/" source-path="/target/m2e-wtp/web-resources"/>
<wb-resource deploy-path="/" source-path="/src/main/webapp" tag="defaultRootSource"/>
<dependent-module archiveName="spring-security-oauth2-1.0.0.BUILD-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/spring-security-oauth2-MITRE/spring-security-oauth2-MITRE">
<dependency-type>uses</dependency-type>
</dependent-module>
<dependent-module archiveName="openid-connect-common-0.1-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/openid-connect-common-MITRE/openid-connect-common-MITRE">
<dependent-module archiveName="openid-connect-common-0.1-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/openid-connect-common/openid-connect-common">
<dependency-type>uses</dependency-type>
</dependent-module>
<property name="java-output-path" value="/openid/target/classes"/>

View File

@ -18,6 +18,7 @@ package org.mitre.oauth2.service.impl;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.model.ClientDetailsEntityFactory;
@ -108,6 +109,15 @@ public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEnt
return client;
}
@Override
public ClientDetailsEntity createClient(ClientDetailsEntity client) {
clientRepository.saveClient(client);
return client;
}
/**
* Delete a client and all its associated tokens
@ -139,6 +149,29 @@ public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEnt
throw new IllegalArgumentException("Neither old client or new client can be null!");
}
/**
*
* @param client object to be saved
* @return ClientDetailsEntity the saved object
*/
@Override
public ClientDetailsEntity saveClient(ClientDetailsEntity client) {
if (client.getClientSecret().equals("")) {
client.setClientSecret(UUID.randomUUID().toString());
}
// this must be a new client if we don't have a clientID
// assign it a new ID
if (client.getClientId() == null || client.getClientId().equals("") || this.loadClientByClientId(client.getClientId()) == null) {
client.setClientId(UUID.randomUUID().toString());
return this.createClient(client);
} else {
return clientRepository.updateClient(client.getClientId(), client);
}
}
/**
* Get all clients in the system
*/

View File

@ -15,16 +15,22 @@
******************************************************************************/
package org.mitre.openid.connect.web;
import com.google.gson.Gson;
import org.mitre.oauth2.exception.ClientNotFoundException;
import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.service.ClientDetailsEntityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.security.Principal;
import java.util.Collection;
import java.util.UUID;
/**
* @author Michael Jett <mjett@mitre.org>
@ -32,6 +38,7 @@ import java.util.Collection;
@Controller
@RequestMapping("/api/clients")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public class ClientAPI {
@Autowired
@ -44,8 +51,7 @@ public class ClientAPI {
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping("")
@RequestMapping(method = RequestMethod.GET, headers="Accept=application/json")
public ModelAndView apiGetAllClients(ModelAndView modelAndView) {
Collection<ClientDetailsEntity> clients = clientService.getAllClients();
@ -55,94 +61,51 @@ public class ClientAPI {
return modelAndView;
}
/*
*/
/**
*
* @param modelAndView
* @param clientId
* @param clientSecret
* @param scope
* @param grantTypes
* @param redirectUri
* @param authorities
* @param name
* @param description
* @param allowRefresh
* @param accessTokenTimeout
* @param refreshTokenTimeout
* @param owner
* @return
*//*
@RequestMapping(method = RequestMethod.POST, headers = "Accept=application/json")
public String apiAddClient(@RequestBody String json, Model m, Principal principal) {
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping("/add")
public ModelAndView apiAddClient(ModelAndView modelAndView,
@RequestParam String clientId, @RequestParam String clientSecret,
@RequestParam String scope, // space delimited
@RequestParam String grantTypes, // space delimited
@RequestParam(required = false) String redirectUri,
@RequestParam String authorities, // space delimited
@RequestParam(required = false) String name,
@RequestParam(required = false) String description,
@RequestParam(required = false, defaultValue = "false") boolean allowRefresh,
@RequestParam(required = false) Long accessTokenTimeout,
@RequestParam(required = false) Long refreshTokenTimeout,
@RequestParam(required = false) String owner
) {
return null;
ClientDetailsEntity client = new Gson().fromJson(json, ClientDetailsEntity.class);
// set owners as current logged in user
client.setOwner(principal.getName());
m.addAttribute("entity", clientService.saveClient(client));
return "jsonClientView";
}
*/
/**
*
* @param modelAndView
* @param clientId
* @return
*//*
@RequestMapping(value="/{id}", method = RequestMethod.PUT, headers = "Accept=application/json")
public String apiUpdateClient(@PathVariable("id") String id, @RequestBody String json, Model m, Principal principal) {
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping("/delete")
public ModelAndView apiDeleteClient(ModelAndView modelAndView,
@RequestParam String clientId) {
return null;
ClientDetailsEntity client = new Gson().fromJson(json, ClientDetailsEntity.class);
client.setClientId(id);
// set owners as current logged in user
client.setOwner(principal.getName());
m.addAttribute("entity", clientService.saveClient(client));
return "jsonClientView";
}
@RequestMapping(value="/{id}", method=RequestMethod.DELETE, headers="Accept=application/json")
public String apiDeleteClient(@PathVariable("id") String id, ModelAndView modelAndView) {
ClientDetailsEntity client = clientService.loadClientByClientId(id);
clientService.deleteClient(client);
return "jsonClientView";
}
*/
@RequestMapping(value="/{id}", method=RequestMethod.GET, headers="Accept=application/json")
@ResponseBody
public Object apiShowClient(@PathVariable("id") Long id, ModelAndView modelAndView) {
ClientDetailsEntity client = clientService.loadClientByClientId(id.toString());
if (client == null) {
return new ResponseEntity<String>(HttpStatus.NOT_FOUND);
}
/* *//**
*
* @param modelAndView
* @param clientId
* @param clientSecret
* @param scope
* @param grantTypes
* @param redirectUri
* @param authorities
* @param name
* @param description
* @param allowRefresh
* @param accessTokenTimeout
* @param refreshTokenTimeout
* @param owner
* @return
*//*
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping("/update")
public ModelAndView apiUpdateClient(ModelAndView modelAndView,
@RequestParam String clientId, @RequestParam String clientSecret,
@RequestParam String scope, // space delimited
@RequestParam String grantTypes, // space delimited
@RequestParam(required = false) String redirectUri,
@RequestParam String authorities, // space delimited
@RequestParam(required = false) String name,
@RequestParam(required = false) String description,
@RequestParam(required = false, defaultValue = "false") boolean allowRefresh,
@RequestParam(required = false) Long accessTokenTimeout,
@RequestParam(required = false) Long refreshTokenTimeout,
@RequestParam(required = false) String owner
) {
return null;
}*/
modelAndView.addObject("entity", client);
modelAndView.setViewName("jsonClientView");
return modelAndView;
}
}

View File

@ -25,18 +25,18 @@ import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/")
@PreAuthorize("hasRole('ROLE_USER')")
public class ManagerController {
@PreAuthorize("hasRole('ROLE_USER')")
@RequestMapping({"", "/home", "/index"})
public String showHomePage() {
return "home";
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping("/admin/manage/clients")
@RequestMapping("/admin/manage/")
public String showClientManager() {
return "admin/manage/clients";
return "admin/manage";
}
}

View File

@ -5,6 +5,7 @@
<script type="text/javascript" src="resources/bootstrap2/js/bootstrap.min.js"></script>
<script type="text/javascript" src="resources/js/underscore-min.js"></script>
<script type="text/javascript" src="resources/js/backbone-min.js"></script>
<script type="text/javascript" src="resources/js/backbone.validations.js"></script>
<script type="text/javascript" src="resources/js/app.js"></script>
</body>

View File

@ -5,7 +5,7 @@
<ul class="nav nav-list">
<security:authorize ifAnyGranted="ROLE_ADMIN">
<li class="nav-header">Administrative</li>
<li><a href="admin/manage/clients">Manage Clients</a></li>
<li><a href="admin/manage/#clients">Manage Clients</a></li>
<li><a href="#">White Lists</a></li>
<li><a href="#">Black Lists</a></li>
</security:authorize>

View File

@ -2,24 +2,88 @@
var ClientModel = Backbone.Model.extend({
// We can pass it default values.
defaults:{
name:null,
redirectURL:"http://myURL.domain",
grantType:["my grant type 1", "my grant type 2"],
scope:["scope 1", "scope 2"],
authority:"my authority",
description:"my description",
refreshTokens:false
idAttribute: "clientId",
initialize: function () {
// bind validation errors to dom elements
// this will display form elements in red if they are not valid
this.bind('error', function(model, errs) {
_.map(errs, function (val, elID) {
$('#' + elID).addClass('error');
});
});
},
urlRoot:"/resources/test/json/clients.js"
validate:{
clientName:{
/* required:true,
pattern:/^[\w ]+$/,
minlength:3,*/
maxlength:100
},
clientDescription:{
/*required:true,
pattern:/^[\w ]+$/,
minlength:3,*/
maxlength:200
},
accessTokenTimeout: {
required: true,
type:"number"
},
refreshTokenTimeout: {
required: true,
type:"number"
},
registeredRedirectUri: {
custom: 'validateURI'
}
},
validateURI: function(attributeName, attributeValue) {
var expression = /^(?:([a-z0-9+.-]+:\/\/)((?:(?:[a-z0-9-._~!$&'()*+,;=:]|%[0-9A-F]{2})*)@)?((?:[a-z0-9-._~!$&'()*+,;=]|%[0-9A-F]{2})*)(:(?:\d*))?(\/(?:[a-z0-9-._~!$&'()*+,;=:@\/]|%[0-9A-F]{2})*)?|([a-z0-9+.-]+:)(\/?(?:[a-z0-9-._~!$&'()*+,;=:@]|%[0-9A-F]{2})+(?:[a-z0-9-._~!$&'()*+,;=:@\/]|%[0-9A-F]{2})*)?)(\?(?:[a-z0-9-._~!$&'()*+,;=:\/?@]|%[0-9A-F]{2})*)?(#(?:[a-z0-9-._~!$&'()*+,;=:\/?@]|%[0-9A-F]{2})*)?$/i;
var regex = new RegExp(expression);
for (var i in attributeValue) {
if (!attributeValue[i].match(regex)) {
return "Invalid URI";
}
}
},
// We can pass it default values.
defaults:{
clientName:"",
clientSecret:"",
registeredRedirectUri:[""],
authorizedGrantTypes:[],
scope:["openid"],
authorities:[],
clientDescription:"",
clientId:null,
allowRefresh:false,
accessTokenTimeout: 0,
refreshTokenTimeout: 0
},
urlRoot:"api/clients"
});
var ClientCollection = Backbone.Collection.extend({
initialize: function() {
this.fetch();
},
model:ClientModel,
url:"/resources/test/json/clients.js"
url:"api/clients"
});
@ -34,7 +98,6 @@
}
this.model.bind('change', this.render, this);
//this.model.on('change', this.render)
},
render:function (eventName) {
@ -48,11 +111,28 @@
},
editClient:function () {
alert('edit');
app.navigate('client/' + this.model.id, {trigger: true});
},
deleteClient:function () {
alert('delete');
if (confirm("Are you sure sure you would like to delete this client?")) {
var self = this;
this.model.destroy({
success:function () {
self.$el.fadeTo("fast", 0.00, function () { //fade
$(this).slideUp("fast", function () { //slide up
$(this).remove(); //then remove from the DOM
});
});
}
});
app.clientListView.delegateEvents();
}
return false;
},
close:function () {
@ -75,7 +155,7 @@
newClient:function () {
this.remove();
document.location.hash = 'new_client';
app.navigate('client/new', {trigger: true});
},
render:function (eventName) {
@ -84,7 +164,7 @@
$(this.el).html($('#tmpl-client-table').html());
_.each(this.model.models, function (client) {
$("#client-table").append(new ClientView({model:client}).render().el);
$("#client-table",this.el).append(new ClientView({model:client}).render().el);
}, this);
return this;
@ -102,15 +182,55 @@
}
},
render:function (eventName) {
events:{
"click .btn-primary":"saveClient"
},
var action = "Edit";
saveClient:function (event) {
if (!this.model) {
action = "New";
$('.control-group').removeClass('error');
var valid = this.model.set({
clientName:$('#clientName input').val(),
clientSecret:$('#clientSecret input').val(),
registeredRedirectUri:$.trim($('#registeredRedirectUri textarea').val()).replace(/ /g,'').split("\n"),
clientDescription:$('#clientDescription textarea').val(),
allowRefresh:$('#allowRefresh').is(':checked'),
accessTokenTimeout: $('#accessTokenTimeout input').val(),
refreshTokenTimeout: $('#refreshTokenTimeout input').val(),
scope:$.map($('#scope textarea').val().replace(/,$/,'').replace(/\s/g,' ').split(","), $.trim)
});
if (valid) {
this.model.save(this.model, {
success:function () {
app.navigate('clients', {trigger:true});
},
error:function () {
}
});
if (this.model.isNew()) {
var self = this;
app.clientList.create(this.model, {
success:function () {
app.navigate('clients', {trigger:true});
},
error:function () {
}
});
}
}
$(this.el).html(this.template({action: action}));
return false;
},
render:function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
@ -119,30 +239,53 @@
var AppRouter = Backbone.Router.extend({
routes:{
"":"list",
"new_client":"newClient"
"clients":"list",
"client/new":"newClient",
"client/:id":"editClient"
},
initialize:function () {
this.clientList = new ClientCollection();
this.clientListView = new ClientListView({model:this.clientList});
this.startAfter([this.clientList]);
},
startAfter:function (collections) {
// Start history when required collections are loaded
var start = _.after(collections.length, _.once(function () {
Backbone.history.start()
}));
_.each(collections, function (collection) {
collection.bind('reset', start, Backbone.history)
});
},
list:function () {
this.clientList = new ClientCollection();
this.clientListView = new ClientListView({model:this.clientList});
this.clientList.fetch();
$('#content').html(this.clientListView.render().el);
this.clientListView.delegateEvents();
},
newClient:function() {
this.clientFormView = new ClientFormView();
this.clientFormView = new ClientFormView({model:new ClientModel()});
$('#content').html(this.clientFormView.render().el);
},
editClient:function(id) {
var client = this.clientList.get(id);
this.clientFormView = new ClientFormView({model:client});
$('#content').html(this.clientFormView.render().el);
}
});
// holds the global app.
// this gets init after the templates load
var app = null;
// main
$(function () {
@ -150,8 +293,7 @@
$.get('resources/template/client.html', function (templates) {
$('body').append(templates);
var app = new AppRouter();
Backbone.history.start();
app = new AppRouter();
});

View File

@ -0,0 +1,286 @@
// Copyright (C) 2011 Neal Stewart
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
(function(Backbone) {
// Premade Validators
Backbone.Validations = {};
var validators = {
"custom" : function(methodName, attributeName, model, valueToSet) {
return model[methodName](attributeName, valueToSet);
},
"required" : function(attributeName, model, valueToSet) {
var currentValue = model.get(attributeName);
var isNotAlreadySet = _.isUndefined(currentValue);
var isNotBeingSet = _.isUndefined(valueToSet);
if (_.isNull(valueToSet) || valueToSet === "" || (isNotBeingSet && isNotAlreadySet)) {
return "required";
} else {
return false;
}
},
"in" : function(whitelist, attributeName, model, valueToSet) {
return _.include(whitelist, valueToSet) ? undefined : "in";
},
"email" : function(type, attributeName, model, valueToSet) {
var emailRegex = new RegExp("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?");
if (_.isString(valueToSet) && !valueToSet.match(emailRegex)) {
return "email";
}
},
"url" : function(type, attributeName, model, valueToSet) {
// taken from jQuery UI validation
var urlRegex = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
if (_.isString(valueToSet) && !valueToSet.match(urlRegex)) {
return "url";
}
},
"number" : function(type, attributeName, model, valueToSet) {
return isNaN(valueToSet) ? 'number' : undefined;
},
"pattern" : function(pattern, attributeName, model, valueToSet) {
if (_.isString(valueToSet)) {
if (pattern.test(valueToSet)) {
return false;
} else {
return "pattern";
}
}
},
"min" : function(minimumValue, attributeName, model, valueToSet) {
if (valueToSet < minimumValue) {
return "min";
}
},
"max" : function(maximumValue, attributeName, model, valueToSet) {
if (valueToSet > maximumValue) {
return "max";
}
},
"minlength" : function(minlength, attributeName, model, valueToSet) {
if (_.isString(valueToSet)) {
if (valueToSet.length < minlength) { return "minlength"; }
}
},
"maxlength" : function(maxlength, attributeName, model, valueToSet) {
if (_.isString(valueToSet)) {
if (valueToSet.length > maxlength) { return "maxlength"; }
}
}
};
var customValidators = {};
var getCustomValidator = function(name) {
var cv = customValidators[name];
if (!cv) { throw "custom validator '"+name+"' could not be found."; }
return cv;
};
Backbone.Validations.addValidator = function(name, validator) {
if (validators.hasOwnProperty(name) || customValidators.hasOwnProperty(name)) {
throw "existing validator";
}
customValidators[name] = validator;
};
/*
The newValidate method overrides validate in Backbone.Model.
It has the same interface as the validate function which you
would provide.
The returned object looks like this:
{
attributeName : ["required", "of", "errors"],
otherAttributeName: ["and", "so", "on"]
}
*/
function newValidate(attributes) {
var errorsForAttribute,
errorHasOccured,
errors = {};
for (var attrName in this._attributeValidators) {
var valueToSet = attributes[attrName];
var validateAttribute = this._attributeValidators[attrName];
if (validateAttribute) {
errorsForAttribute = validateAttribute(this, valueToSet);
}
if (errorsForAttribute) {
errorHasOccured = true;
errors[attrName] = errorsForAttribute;
}
}
return errorHasOccured ? errors : false;
}
function createMinValidator(attributeName, minimumValue) {
return _.bind(validators.min, null, minimumValue);
}
function createMaxValidator(attributeName, maximumValue) {
return _.bind(validators.max, null, maximumValue);
}
/* createValidator takes in:
- the model
- the name of the attribute
- the type of validation
- the description of the validation
returns a function that takes in:
- the value being set for the attribute
and either returns nothing (undefined),
or the error name (string).
*/
function createValidator(attributeName, type, description) {
var validator,
validatorMethod,
customValidator;
if (type === "type") {
type = description;
}
validator = validators[type];
if (!validator) { validator = getCustomValidator(type); }
if (!validator) { throw "Improper validation type '"+type+"'" ; }
if (type !== "required") { // doesn't need the description
validator = _.bind(validator, null, description, attributeName);
} else {
validator = _.bind(validator, null, attributeName);
}
return validator;
}
function createAttributeValidator(attributeName, attributeDescription) {
var validatorsForAttribute = [],
type,
desc;
for (type in attributeDescription) {
desc = attributeDescription[type];
validatorsForAttribute.push(createValidator(attributeName, type, desc));
}
return function(model, valueToSet, hasOverridenError, options) {
var validator,
result,
errors = [];
for (var i = 0, length = validatorsForAttribute.length; i < length; i++) {
validator = validatorsForAttribute[i];
result = validator(model, valueToSet);
if (result) {
if (_.isArray(result)) {
errors = errors.concat(result);
} else {
errors.push(result);
}
}
}
if (errors.length) {
return errors;
} else {
return false;
}
};
}
function createValidators(modelValidations) {
var attributeValidations,
attributeValidators = {};
for (var attrName in modelValidations) {
attributeValidations = modelValidations[attrName];
attributeValidators[attrName] = createAttributeValidator(attrName, attributeValidations);
}
return attributeValidators;
}
var oldPerformValidation = Backbone.Model.prototype._performValidation;
function newPerformValidation(attrs, options) {
if (options.silent || !this.validate) return true;
var errors = this.validate(attrs);
if (errors) {
if (options.error) {
options.error(this, errors, options);
} else {
this.trigger('error', this, errors, options);
_.each(errors, function(error, name) {
this.trigger('error:' + name, this, errors, options);
}, this);
}
return false;
}
return true;
}
// save the old backbone
var oldModel = Backbone.Model;
// Constructor for our new Validations Model
Backbone.Validations.Model = Backbone.Model.extend({
constructor : function() {
// if they pass an object, construct the new validations
if (typeof this.validate === "object" && this.validate !== null) {
if (!this.constructor.prototype._attributeValidators) {
this.constructor.prototype._attributeValidators = createValidators(this.validate);
this.constructor.prototype.validate = newValidate;
this.constructor.prototype._validate = newPerformValidation;
}
}
oldModel.apply(this, arguments);
}
});
// Override Backbone.Model with our new Model
Backbone.Model = Backbone.Validations.Model;
// Requisite noConflict
Backbone.Validations.Model.noConflict = function() {
Backbone.Model = oldModel;
};
}(Backbone));

View File

@ -1,16 +1,22 @@
<script type="text/html" id="tmpl-client">
<td>
<%=name%>
</td>
<td>
<%=redirectURL%>
<%=clientName%>
</td>
<td>
<ul>
<% for (var i in grantType) { %>
<% for (var i in registeredRedirectUri) { %>
<li>
<%=grantType[i]%>
<%=registeredRedirectUri[i]%>
</li>
<% } %>
</ul>
</td>
<td>
<ul>
<% for (var i in authorizedGrantTypes) { %>
<li>
<%=authorizedGrantTypes[i]%>
</li>
<% } %>
</ul>
@ -24,14 +30,12 @@
<% } %>
</ul>
</td>
<td>
<%=authority%>
</td>
<td>
<%=description%>
<%=clientDescription%>
</td>
<td><input type="checkbox"
<%=(refreshTokens == 1 ? 'checked' : '')%>
<%=(allowRefresh == 1 ? 'checked' : '')%>
value="" id="" name="" disabled>
</td>
<td>
@ -52,10 +56,9 @@
<thead>
<tr>
<th>Name</th>
<th>Redirect URL</th>
<th>Redirect URI(s)</th>
<th>Grant Types</th>
<th>Scope</th>
<th>Authority</th>
<th>Description</th>
<th>Refresh Tokens</th>
<th class="span1"></th>
@ -74,7 +77,7 @@
<script type="text/html" id="tmpl-client-form">
<h1><%=action%> Client</h1>
<h1><%=(clientId == null ? 'New' : 'Edit')%> Client</h1>
<div class="">
@ -86,16 +89,24 @@
<div class="row-fluid">
<div class="span6">
<span class="control-group" id="clientName">
<label>Client name</label>
<input type="text" class="" placeholder="Type something"> <span class="help-inline">Associated help text!</span>
<label>Refresh URL</label>
<input type="text" class="" placeholder="http://"><span class="help-inline">Associated help text!</span>
<input value="<%=clientName%>" maxlength="100" type="text" class="" placeholder="Type something"> <span class="help-inline"></span>
</span>
<span class="control-group" id="registeredRedirectUri">
<label>Redirect URI(s)</label>
<textarea class="input-xlarge" placeholder="http://"
rows="3"><% for (var i in registeredRedirectUri) { %><%=registeredRedirectUri[i]+"\n"%><% } %></textarea> <span class="help-inline">You may enter multiple URIs separated by a new lines</span>
</span>
</div>
<div class="span6">
<span class="control-group" id="clientDescription">
<label>Description</label>
<textarea class="input-xlarge" id="textarea" placeholder="Type a description"
rows="3"></textarea> <span class="help-inline">Associated help text!</span>
<textarea class="input-xlarge" placeholder="Type a description" maxlength="200"
rows="3"><%=clientDescription%></textarea> <span class="help-inline"></span>
</span>
</div>
</div>
@ -106,9 +117,9 @@
token after it
expires.</p>
<label class="checkbox">
<input type="checkbox"> Allow refresh tokens?
<input type="checkbox" id="allowRefresh" <%=(allowRefresh == true ? 'checked' : '')%>> Allow refresh tokens?
</label>
<button type="submit" class="btn btn-primary">Submit</button>
<button class="btn btn-primary">Submit</button>
</div>
</div>
</fieldset>
@ -117,93 +128,53 @@
<div class="row-fluid">
<div class="span4 control-group">
<div class="span4 control-group" id="scope">
<label class="control-label">Grant Types</label>
<label class="control-label">Scope</label>
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="grant-type-checkbox-list1" value="option1">
Option one
</label>
<label class="checkbox">
<input type="checkbox" name="grant-type-checkbox-list2" value="option2">
Option two
</label>
<label class="checkbox">
<input type="checkbox" name="grant-type-checkbox-list3" value="option3">
Option three
</label>
<p class="help-block"><strong>Note:</strong> Labels surround all the options for
much larger click areas and a more usable form.</p>
</div>
<textarea rows="3" class="xlarge" placeholder="openid"
id="textarea2" name="textarea2"><% for (var i in scope) { %><%=scope[i]+","%><% }%></textarea>
<span class="help-block">
Please enter scopes separated by commas
</span>
</div>
<div class="span4 control-group">
<div class="span4 control-group" id="clientSecret">
<label class="control-label">Auth Types</label>
<div class="controls">
<label class="checkbox">
<input type="checkbox" name="auth-type-checkbox-list1" value="option1">
Option one
</label>
<label class="checkbox">
<input type="checkbox" name="auth-type-checkbox-list2" value="option2">
Option two
</label>
<label class="checkbox">
<input type="checkbox" name="auth-type-checkbox-list3" value="option3">
Option three
</label>
<p class="help-block"><strong>Note:</strong> Labels surround all the options for
much larger click areas and a more usable form.</p>
</div>
<label>Client Secret</label>
<input value="<%=clientSecret%>" maxlength="100" type="text" class=""
placeholder="Type a secret"> <span class="help-inline">If you leave this blank a client secret will be generated for you automatically.</span>
</div>
<div class="span4 control-group">
<div class="span4">
<span class="control-group" id="accessTokenTimeout">
<label class="control-label" for="access-token-timeout-seconds" style="">Access Token
Timeout</label>
<div class="controls form-horizontal" style="">
<div class="input-append">
<input type="text" class="" id="access-token-timeout-seconds" size="16"><span
<input type="text" class="" value="<%=accessTokenTimeout%>" id="access-token-timeout-seconds" size="16"><span
class="add-on">seconds</span>
</div>
<span class="help-inline">Here's more help text</span>
</div>
</span>
<span class="control-group" id="refreshTokenTimeout">
<label class="control-label" for="refresh-token-timeout-seconds" style="">Refresh Token
Timeout</label>
<div class="controls form-horizontal" style="">
<div class="input-append">
<input type="text" class="" id="refresh-token-timeout-seconds" size="16"><span
<input type="text" class="" value="<%=refreshTokenTimeout%>" id="refresh-token-timeout-seconds" size="16"><span
class="add-on">seconds</span>
</div>
<span class="help-inline">Here's more help text</span>
</div>
</span>
</div>
<div class="span12 control-group">
<label for="textarea2">Scope</label>
<div class="input">
<textarea rows="3" class="xlarge span10" placeholder="email,first name, last name"
id="textarea2" name="textarea2"></textarea>
<span class="help-block">
Please enter scopes separated by commas
</span>
</div>
</div>
</div>
</fieldset>
</form>