major refactor of client filter
Collapsed filter into single class pulled server config and client config management into service classes created service for issuer (will handle account chooser) created auth request services (handle signed and unsigned requests)pull/306/merge
parent
2b45dd1104
commit
f44c704472
|
@ -65,7 +65,7 @@ Additionally, it contains a set of convenience methods to pass through to parame
|
|||
Configure like so:
|
||||
|
||||
<bean id="openIdConnectAuthenticationFilter"
|
||||
class="org.mitre.openid.connect.client.OIDCAuthenticationFilter">
|
||||
class="org.mitre.openid.connect.client.PlainOIDCAuthenticationFilter">
|
||||
<property name="authenticationManager" ref="authenticationManager" />
|
||||
<property name="errorRedirectURI" value="/login.jsp?authfail=openid" />
|
||||
<property name="issuer" value="http://server.example.com:8080/openid-connect-server/" />
|
||||
|
|
|
@ -1,613 +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.openid.connect.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.utils.URLEncodedUtils;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
|
||||
import org.mitre.jwt.signer.service.impl.JWKSetSigningAndValidationServiceCacheService;
|
||||
import org.mitre.openid.connect.config.OIDCServerConfiguration;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
|
||||
/**
|
||||
* Abstract OpenID Connect Authentication Filter class
|
||||
*
|
||||
* @author nemonik
|
||||
*
|
||||
*/
|
||||
public class AbstractOIDCAuthenticationFilter extends
|
||||
AbstractAuthenticationProcessingFilter {
|
||||
|
||||
protected static final String REDIRECT_URI_SESION_VARIABLE = "redirect_uri";
|
||||
protected static final String STATE_SESSION_VARIABLE = "state";
|
||||
protected final static String NONCE_SESSION_VARIABLE = "nonce";
|
||||
protected final static int HTTP_SOCKET_TIMEOUT = 30000;
|
||||
protected final static String DEFAULT_SCOPE = "openid";
|
||||
|
||||
protected final static String FILTER_PROCESSES_URL = "/openid_connect_login";
|
||||
|
||||
// Allow for time sync issues by having a window of X seconds.
|
||||
private int timeSkewAllowance = 300;
|
||||
|
||||
@Autowired
|
||||
JWKSetSigningAndValidationServiceCacheService validationServices;
|
||||
|
||||
/**
|
||||
* Builds the redirect_uri that will be sent to the Authorization Endpoint.
|
||||
* By default returns the URL of the current request minus zero or more
|
||||
* fields of the URL's query string.
|
||||
*
|
||||
* @param request
|
||||
* the current request which is being processed by this filter
|
||||
* @param ignoreFields
|
||||
* an array of field names to ignore.
|
||||
* @return a URL built from the messaged parameters.
|
||||
*/
|
||||
public static String buildRedirectURI(HttpServletRequest request, String[] ignoreFields) {
|
||||
|
||||
List<String> ignore = (ignoreFields != null) ? Arrays.asList(ignoreFields) : null;
|
||||
|
||||
//boolean isFirst = true;
|
||||
|
||||
StringBuffer sb = request.getRequestURL();
|
||||
List<NameValuePair> queryparams = new ArrayList<NameValuePair>();
|
||||
|
||||
|
||||
for (Enumeration<?> e = request.getParameterNames(); e.hasMoreElements();) {
|
||||
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
queryparams.add(new BasicNameValuePair(name,value));
|
||||
|
||||
//if (isFirst) {
|
||||
// sb.append("?");
|
||||
// isFirst = false;
|
||||
//}
|
||||
|
||||
//sb.append(name).append("=").append(value);
|
||||
|
||||
//if (e.hasMoreElements()) {
|
||||
// sb.append("&");
|
||||
//}
|
||||
}
|
||||
|
||||
}
|
||||
return sb.append("?").append(URLEncodedUtils.format(queryparams, "UTF-8")).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
List<NameValuePair> queryparams = new ArrayList<NameValuePair>();
|
||||
char appendChar = '?';
|
||||
|
||||
// build a NameValuePair list for the query paramaters
|
||||
for (Map.Entry<String, String> param : queryStringFields.entrySet()){
|
||||
queryparams.add(new BasicNameValuePair(param.getKey(),param.getValue()));
|
||||
}
|
||||
URLBuilder.append(appendChar).append(URLEncodedUtils.format(queryparams, "UTF-8"));
|
||||
|
||||
return URLBuilder.toString();
|
||||
}
|
||||
|
||||
protected String errorRedirectURI;
|
||||
|
||||
protected String scope;
|
||||
|
||||
protected int httpSocketTimeout = HTTP_SOCKET_TIMEOUT;
|
||||
|
||||
/**
|
||||
* OpenIdConnectAuthenticationFilter constructor
|
||||
*/
|
||||
protected AbstractOIDCAuthenticationFilter() {
|
||||
super(FILTER_PROCESSES_URL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
super.afterPropertiesSet();
|
||||
|
||||
Assert.notNull(errorRedirectURI, "An Error Redirect URI must be supplied");
|
||||
|
||||
if (Strings.isNullOrEmpty(scope)) {
|
||||
setScope(DEFAULT_SCOPE);
|
||||
} else {
|
||||
setScope(scope);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (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 AuthenticationException, IOException, ServletException {
|
||||
|
||||
logger.debug("Request: "
|
||||
+ request.getRequestURI()
|
||||
+ (StringUtils.isNotBlank(request.getQueryString()) ? "?"
|
||||
+ request.getQueryString() : ""));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the authorization grant response
|
||||
*
|
||||
* @param authorizationCode
|
||||
* 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 Exception
|
||||
* @throws UnsupportedEncodingException
|
||||
*/
|
||||
protected Authentication handleAuthorizationGrantResponse(String authorizationCode, HttpServletRequest request, OIDCServerConfiguration serverConfig) {
|
||||
|
||||
final boolean debug = logger.isDebugEnabled();
|
||||
|
||||
HttpSession session = request.getSession();
|
||||
|
||||
// check for state
|
||||
String storedState = getStoredState(session);
|
||||
if (!StringUtils.isBlank(storedState)) {
|
||||
String state = request.getParameter("state");
|
||||
if (!storedState.equals(state)) {
|
||||
throw new AuthenticationServiceException("State parameter mismatch on return. Expected " + storedState + " got " + state);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Token Endpoint interaction
|
||||
HttpClient httpClient = new DefaultHttpClient();
|
||||
|
||||
httpClient.getParams().setParameter("http.socket.timeout", new Integer(httpSocketTimeout));
|
||||
|
||||
//
|
||||
// TODO: basic auth is untested (it wasn't working last I
|
||||
// tested)
|
||||
// UsernamePasswordCredentials credentials = new
|
||||
// UsernamePasswordCredentials(serverConfig.getClientId(),
|
||||
// serverConfig.getClientSecret());
|
||||
// ((DefaultHttpClient)
|
||||
// httpClient).getCredentialsProvider().setCredentials(AuthScope.ANY,
|
||||
// credentials);
|
||||
//
|
||||
|
||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate(factory);
|
||||
|
||||
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
|
||||
form.add("grant_type", "authorization_code");
|
||||
form.add("code", authorizationCode);
|
||||
|
||||
String redirectUri = getStoredRedirectUri(session);
|
||||
if (redirectUri != null) {
|
||||
form.add("redirect_uri", redirectUri);
|
||||
}
|
||||
|
||||
// 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.getTokenEndpointUrl());
|
||||
logger.debug("form = " + form);
|
||||
}
|
||||
|
||||
String jsonString = null;
|
||||
|
||||
try {
|
||||
jsonString = restTemplate.postForObject(
|
||||
serverConfig.getTokenEndpointUrl(), form, String.class);
|
||||
} catch (HttpClientErrorException httpClientErrorException) {
|
||||
|
||||
// Handle error
|
||||
|
||||
logger.error("Token Endpoint error response: "
|
||||
+ httpClientErrorException.getStatusText() + " : "
|
||||
+ httpClientErrorException.getMessage());
|
||||
|
||||
throw new AuthenticationServiceException(
|
||||
"Unable to obtain Access Token.");
|
||||
}
|
||||
|
||||
logger.debug("from TokenEndpoint jsonString = " + jsonString);
|
||||
|
||||
JsonElement jsonRoot = new JsonParser().parse(jsonString);
|
||||
if (!jsonRoot.isJsonObject()) {
|
||||
throw new AuthenticationServiceException("Token Endpoint did not return a JSON object: " + jsonRoot);
|
||||
}
|
||||
|
||||
JsonObject tokenResponse = jsonRoot.getAsJsonObject();
|
||||
|
||||
if (tokenResponse.get("error") != null) {
|
||||
|
||||
// Handle error
|
||||
|
||||
String error = tokenResponse.get("error")
|
||||
.getAsString();
|
||||
|
||||
logger.error("Token Endpoint returned: " + error);
|
||||
|
||||
throw new AuthenticationServiceException("Unable to obtain Access Token. Token Endpoint returned: " + error);
|
||||
|
||||
} else {
|
||||
|
||||
// Extract the id_token to insert into the
|
||||
// OIDCAuthenticationToken
|
||||
|
||||
// get out all the token strings
|
||||
String accessTokenValue = null;
|
||||
String idTokenValue = null;
|
||||
String refreshTokenValue = null;
|
||||
|
||||
if (tokenResponse.has("access_token")) {
|
||||
accessTokenValue = tokenResponse.get("access_token").getAsString();
|
||||
} else {
|
||||
throw new AuthenticationServiceException("Token Endpoint did not return an access_token: " + jsonString);
|
||||
}
|
||||
|
||||
if (tokenResponse.has("id_token")) {
|
||||
idTokenValue = tokenResponse.get("id_token").getAsString();
|
||||
} else {
|
||||
logger.error("Token Endpoint did not return an id_token");
|
||||
throw new AuthenticationServiceException("Token Endpoint did not return an id_token");
|
||||
}
|
||||
|
||||
if (tokenResponse.has("refresh_token")) {
|
||||
refreshTokenValue = tokenResponse.get("refresh_token").getAsString();
|
||||
}
|
||||
|
||||
try {
|
||||
SignedJWT idToken = SignedJWT.parse(idTokenValue);
|
||||
|
||||
// validate our ID Token over a number of tests
|
||||
ReadOnlyJWTClaimsSet idClaims = idToken.getJWTClaimsSet();
|
||||
|
||||
// check the signature
|
||||
JwtSigningAndValidationService jwtValidator = validationServices.get(serverConfig.getJwkSigningUrl());
|
||||
if (jwtValidator != null) {
|
||||
if(!jwtValidator.validateSignature(idToken)) {
|
||||
throw new AuthenticationServiceException("Signature validation failed");
|
||||
}
|
||||
} else {
|
||||
logger.info("No validation service found. Skipping signature validation");
|
||||
}
|
||||
|
||||
// check the issuer
|
||||
if (idClaims.getIssuer() == null) {
|
||||
throw new AuthenticationServiceException("Id Token Issuer is null");
|
||||
} else if (!idClaims.getIssuer().equals(serverConfig.getIssuer())){
|
||||
throw new AuthenticationServiceException("Issuers do not match, expected " + serverConfig.getIssuer() + " got " + idClaims.getIssuer());
|
||||
}
|
||||
|
||||
// check expiration
|
||||
if (idClaims.getExpirationTime() == null) {
|
||||
throw new AuthenticationServiceException("Id Token does not have required expiration claim");
|
||||
} else {
|
||||
// it's not null, see if it's expired
|
||||
Date now = new Date(System.currentTimeMillis() - (timeSkewAllowance * 1000));
|
||||
if (now.after(idClaims.getExpirationTime())) {
|
||||
throw new AuthenticationServiceException("Id Token is expired: " + idClaims.getExpirationTime());
|
||||
}
|
||||
}
|
||||
|
||||
// check not before
|
||||
if (idClaims.getNotBeforeTime() != null) {
|
||||
Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000));
|
||||
if (now.before(idClaims.getNotBeforeTime())){
|
||||
throw new AuthenticationServiceException("Id Token not valid untill: " + idClaims.getNotBeforeTime());
|
||||
}
|
||||
}
|
||||
|
||||
// check issued at
|
||||
if (idClaims.getIssueTime() == null) {
|
||||
throw new AuthenticationServiceException("Id Token does not have required issued-at claim");
|
||||
} else {
|
||||
// since it's not null, see if it was issued in the future
|
||||
Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000));
|
||||
if (now.before(idClaims.getIssueTime())) {
|
||||
throw new AuthenticationServiceException("Id Token was issued in the future: " + idClaims.getIssueTime());
|
||||
}
|
||||
}
|
||||
|
||||
// check audience
|
||||
if (idClaims.getAudience() == null) {
|
||||
throw new AuthenticationServiceException("Id token audience is null");
|
||||
} else if (!idClaims.getAudience().contains(serverConfig.getClientId())) {
|
||||
throw new AuthenticationServiceException("Audience does not match, expected " + serverConfig.getClientId() + " got " + idClaims.getAudience());
|
||||
}
|
||||
|
||||
// compare the nonce to our stored claim
|
||||
// FIXME: Nimbus claims as strings?
|
||||
String nonce = (String) idClaims.getCustomClaim("nonce");
|
||||
if (StringUtils.isBlank(nonce)) {
|
||||
|
||||
logger.error("ID token did not contain a nonce claim.");
|
||||
|
||||
throw new AuthenticationServiceException("ID token did not contain a nonce claim.");
|
||||
}
|
||||
|
||||
String storedNonce = getStoredNonce(session);
|
||||
if (!nonce.equals(storedNonce)) {
|
||||
logger.error("Possible replay attack detected! The comparison of the nonce in the returned "
|
||||
+ "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + ".");
|
||||
|
||||
throw new AuthenticationServiceException(
|
||||
"Possible replay attack detected! The comparison of the nonce in the returned "
|
||||
+ "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + ".");
|
||||
}
|
||||
|
||||
// pull the subject (user id) out as a claim on the id_token
|
||||
|
||||
String userId = idClaims.getSubject();
|
||||
|
||||
// construct an OIDCAuthenticationToken and return a Authentication object w/the userId and the idToken
|
||||
|
||||
OIDCAuthenticationToken token = new OIDCAuthenticationToken(userId, idClaims.getIssuer(), serverConfig, idTokenValue, accessTokenValue, refreshTokenValue);
|
||||
|
||||
Authentication authentication = this.getAuthenticationManager().authenticate(token);
|
||||
|
||||
return authentication;
|
||||
} catch (ParseException e) {
|
||||
throw new AuthenticationServiceException("Couldn't parse idToken: ", e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate an Authorization request
|
||||
*
|
||||
* @param request
|
||||
* The request from which to extract parameters and perform the
|
||||
* authentication
|
||||
* @param response
|
||||
* The response, needed to set a cookie and do a redirect as part
|
||||
* of a multi-stage authentication process
|
||||
* @param serverConfiguration
|
||||
* @throws IOException
|
||||
* If an input or output exception occurs
|
||||
*/
|
||||
protected void handleAuthorizationRequest(HttpServletRequest request,
|
||||
HttpServletResponse response, OIDCServerConfiguration serverConfiguration) throws IOException {
|
||||
|
||||
HttpSession session = request.getSession();
|
||||
|
||||
Map<String, String> urlVariables = new HashMap<String, String>();
|
||||
|
||||
// Required parameters:
|
||||
|
||||
urlVariables.put("response_type", "code");
|
||||
urlVariables.put("client_id", serverConfiguration.getClientId());
|
||||
urlVariables.put("scope", scope);
|
||||
|
||||
String redirectURI = buildRedirectURI(request, null);
|
||||
urlVariables.put("redirect_uri", redirectURI);
|
||||
session.setAttribute(REDIRECT_URI_SESION_VARIABLE, redirectURI);
|
||||
|
||||
// Create a string value used to associate a user agent session
|
||||
// with an ID Token to mitigate replay attacks. The value is
|
||||
// passed through unmodified to the ID Token. One method is to
|
||||
// store a random value as a signed session cookie, and pass the
|
||||
// value in the nonce parameter.
|
||||
|
||||
String nonce = createNonce(session);
|
||||
urlVariables.put("nonce", nonce);
|
||||
|
||||
String state = createState(session);
|
||||
urlVariables.put("state", state);
|
||||
|
||||
// Optional parameters:
|
||||
|
||||
// TODO: display, prompt, request, request_uri
|
||||
|
||||
String authRequest = AbstractOIDCAuthenticationFilter.buildURL(serverConfiguration.getAuthorizationEndpointUrl(), urlVariables);
|
||||
|
||||
logger.debug("Auth Request: " + authRequest);
|
||||
|
||||
response.sendRedirect(authRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Authorization Endpoint error
|
||||
*
|
||||
* @param request
|
||||
* The request from which to extract parameters and handle the
|
||||
* error
|
||||
* @param response
|
||||
* The response, needed to do a redirect to display the error
|
||||
* @throws IOException
|
||||
* If an input or output exception occurs
|
||||
*/
|
||||
protected void handleError(HttpServletRequest request,
|
||||
HttpServletResponse response) throws IOException {
|
||||
|
||||
String error = request.getParameter("error");
|
||||
String errorDescription = request.getParameter("error_description");
|
||||
String errorURI = request.getParameter("error_uri");
|
||||
|
||||
Map<String, String> requestParams = new HashMap<String, String>();
|
||||
|
||||
requestParams.put("error", error);
|
||||
|
||||
if (errorDescription != null) {
|
||||
requestParams.put("error_description", errorDescription);
|
||||
}
|
||||
|
||||
if (errorURI != null) {
|
||||
requestParams.put("error_uri", errorURI);
|
||||
}
|
||||
|
||||
response.sendRedirect(AbstractOIDCAuthenticationFilter.buildURL(
|
||||
errorRedirectURI, requestParams));
|
||||
}
|
||||
|
||||
public void setErrorRedirectURI(String errorRedirectURI) {
|
||||
this.errorRedirectURI = errorRedirectURI;
|
||||
}
|
||||
|
||||
public void setScope(String scope) {
|
||||
this.scope = scope;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the named stored session variable as a string. Return null if not found or not a string.
|
||||
* @param session
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
private static String getStoredSessionString(HttpSession session, String key) {
|
||||
Object o = session.getAttribute(key);
|
||||
if (o != null && o instanceof String) {
|
||||
return o.toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cryptographically random nonce and store it in the session
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
protected static String createNonce(HttpSession session) {
|
||||
String nonce = new BigInteger(50, new SecureRandom()).toString(16);
|
||||
session.setAttribute(NONCE_SESSION_VARIABLE, nonce);
|
||||
|
||||
return nonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nonce we stored in the session
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
protected static String getStoredNonce(HttpSession session) {
|
||||
return getStoredSessionString(session, NONCE_SESSION_VARIABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cryptographically random state and store it in the session
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
protected static String createState(HttpSession session) {
|
||||
String state = new BigInteger(50, new SecureRandom()).toString(16);
|
||||
session.setAttribute(STATE_SESSION_VARIABLE, state);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state we stored in the session
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
protected static String getStoredState(HttpSession session) {
|
||||
return getStoredSessionString(session, STATE_SESSION_VARIABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored redirect URI that we used on the way out
|
||||
* @param serverConfig
|
||||
* @return
|
||||
*/
|
||||
protected static String getStoredRedirectUri(HttpSession session) {
|
||||
return getStoredSessionString(session, REDIRECT_URI_SESION_VARIABLE);
|
||||
}
|
||||
|
||||
public int getTimeSkewAllowance() {
|
||||
return timeSkewAllowance;
|
||||
}
|
||||
|
||||
public void setTimeSkewAllowance(int timeSkewAllowance) {
|
||||
this.timeSkewAllowance = timeSkewAllowance;
|
||||
}
|
||||
|
||||
}
|
|
@ -16,141 +16,465 @@
|
|||
package org.mitre.openid.connect.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.mitre.openid.connect.config.OIDCServerConfiguration;
|
||||
import org.apache.http.auth.AuthScope;
|
||||
import org.apache.http.auth.UsernamePasswordCredentials;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
|
||||
import org.mitre.jwt.signer.service.impl.JWKSetSigningAndValidationServiceCacheService;
|
||||
import org.mitre.openid.connect.client.service.AuthRequestUrlBuilder;
|
||||
import org.mitre.openid.connect.client.service.ClientConfigurationService;
|
||||
import org.mitre.openid.connect.client.service.IssuerService;
|
||||
import org.mitre.openid.connect.client.service.ServerConfigurationService;
|
||||
import org.mitre.openid.connect.config.ServerConfiguration;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
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.security.oauth2.provider.ClientDetails;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
|
||||
/**
|
||||
* The OpenID Connect Authentication Filter
|
||||
* OpenID Connect Authentication Filter class
|
||||
*
|
||||
* See README.md to to configure
|
||||
*
|
||||
* @author nemonik
|
||||
* @author nemonik, jricher
|
||||
*
|
||||
*/
|
||||
public class OIDCAuthenticationFilter extends AbstractOIDCAuthenticationFilter {
|
||||
public class OIDCAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
|
||||
|
||||
protected OIDCServerConfiguration oidcServerConfig;
|
||||
protected final static String REDIRECT_URI_SESION_VARIABLE = "redirect_uri";
|
||||
protected final static String STATE_SESSION_VARIABLE = "state";
|
||||
protected final static String NONCE_SESSION_VARIABLE = "nonce";
|
||||
protected final static String ISSUER_SESSION_VARIABLE = "issuer";
|
||||
protected final static int HTTP_SOCKET_TIMEOUT = 30000;
|
||||
|
||||
protected final static String FILTER_PROCESSES_URL = "/openid_connect_login";
|
||||
|
||||
// Allow for time sync issues by having a window of X seconds.
|
||||
private int timeSkewAllowance = 300;
|
||||
|
||||
@Autowired
|
||||
private JWKSetSigningAndValidationServiceCacheService validationServices;
|
||||
|
||||
// modular services to build out client filter
|
||||
private ServerConfigurationService servers;
|
||||
private ClientConfigurationService clients;
|
||||
private IssuerService issuerService;
|
||||
private AuthRequestUrlBuilder authRequestBuilder;
|
||||
|
||||
protected int httpSocketTimeout = HTTP_SOCKET_TIMEOUT;
|
||||
|
||||
/**
|
||||
* OpenIdConnectAuthenticationFilter constructor
|
||||
*/
|
||||
protected OIDCAuthenticationFilter() {
|
||||
super();
|
||||
|
||||
oidcServerConfig = new OIDCServerConfiguration();
|
||||
super(FILTER_PROCESSES_URL);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.mitre.openid.connect.client.AbstractOIDCAuthenticationFilter#
|
||||
* afterPropertiesSet()
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
super.afterPropertiesSet();
|
||||
|
||||
// Validating configuration
|
||||
|
||||
Assert.notNull(oidcServerConfig.getAuthorizationEndpointUrl(), "An Authorization Endpoint URI must be supplied");
|
||||
|
||||
Assert.notNull(oidcServerConfig.getTokenEndpointUrl(), "A Token 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");
|
||||
}
|
||||
|
||||
/*
|
||||
* This is the main entry point for the filter.
|
||||
*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see org.mitre.openid.connect.client.AbstractOIDCAuthenticationFilter#
|
||||
* attemptAuthentication(javax.servlet.http.HttpServletRequest,
|
||||
* @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 AuthenticationException,
|
||||
IOException, ServletException {
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
|
||||
|
||||
// Enter AuthenticationFilter here...
|
||||
|
||||
super.attemptAuthentication(request, response);
|
||||
|
||||
if (StringUtils.isNotBlank(request.getParameter("error"))) {
|
||||
if (!Strings.isNullOrEmpty(request.getParameter("error"))) {
|
||||
|
||||
// there's an error coming back from the server, need to handle this
|
||||
handleError(request, response);
|
||||
return null; // no auth, response is sent to display page or something
|
||||
|
||||
} else if (!Strings.isNullOrEmpty(request.getParameter("code"))) {
|
||||
|
||||
} else if (StringUtils.isNotBlank(request.getParameter("code"))) {
|
||||
// we got back the code, need to process this to get our tokens
|
||||
Authentication auth = handleAuthorizationCodeResponse(request, response);
|
||||
return auth;
|
||||
|
||||
} else {
|
||||
|
||||
// not an error, not a code, must be an initial login of some type
|
||||
handleAuthorizationRequest(request, response);
|
||||
|
||||
return null; // no auth, response redirected to the server's Auth Endpoint (or possibly to the account chooser)
|
||||
}
|
||||
|
||||
try {
|
||||
return handleAuthorizationGrantResponse(request.getParameter("code"), request, oidcServerConfig);
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate an Authorization request
|
||||
*
|
||||
* @param request
|
||||
* The request from which to extract parameters and perform the
|
||||
* authentication
|
||||
* @param response
|
||||
* @throws IOException
|
||||
* If an input or output exception occurs
|
||||
*/
|
||||
protected void handleAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
|
||||
HttpSession session = request.getSession();
|
||||
|
||||
String issuer = issuerService.getIssuer(request);
|
||||
ServerConfiguration serverConfig = servers.getServerConfiguration(issuer);
|
||||
ClientDetails clientConfig = clients.getClientConfiguration(issuer);
|
||||
|
||||
// our redirect URI is this current URL, with no query parameters
|
||||
String redirectUri = request.getRequestURL().toString();
|
||||
session.setAttribute(REDIRECT_URI_SESION_VARIABLE, redirectUri);
|
||||
|
||||
// this value comes back in the id token and is checked there
|
||||
String nonce = createNonce(session);
|
||||
|
||||
// this value comes back in the auth code response
|
||||
String state = createState(session);
|
||||
|
||||
String authRequest = authRequestBuilder.buildAuthRequestUrl(serverConfig, clientConfig, redirectUri, nonce, state);
|
||||
|
||||
logger.debug("Auth Request: " + authRequest);
|
||||
|
||||
response.sendRedirect(authRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param request
|
||||
* The request from which to extract parameters and perform the
|
||||
* authentication
|
||||
* @return The authenticated user token, or null if authentication is
|
||||
* incomplete.
|
||||
*/
|
||||
protected Authentication handleAuthorizationCodeResponse(HttpServletRequest request, HttpServletResponse response) {
|
||||
|
||||
String authorizationCode = request.getParameter("code");
|
||||
|
||||
HttpSession session = request.getSession();
|
||||
|
||||
// check for state, if it doesn't match we bail early
|
||||
String storedState = getStoredState(session);
|
||||
if (!StringUtils.isBlank(storedState)) {
|
||||
String state = request.getParameter("state");
|
||||
if (!storedState.equals(state)) {
|
||||
throw new AuthenticationServiceException("State parameter mismatch on return. Expected " + storedState + " got " + state);
|
||||
}
|
||||
}
|
||||
|
||||
// look up the issuer that we set out to talk to
|
||||
String issuer = getStoredSessionString(session, ISSUER_SESSION_VARIABLE);
|
||||
|
||||
// pull the configurations based on that issuer
|
||||
ServerConfiguration serverConfig = servers.getServerConfiguration(issuer);
|
||||
ClientDetails clientConfig = clients.getClientConfiguration(issuer);
|
||||
|
||||
|
||||
// Handle Token Endpoint interaction
|
||||
DefaultHttpClient httpClient = new DefaultHttpClient();
|
||||
|
||||
httpClient.getParams().setParameter("http.socket.timeout", new Integer(httpSocketTimeout));
|
||||
|
||||
|
||||
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(clientConfig.getClientId(), clientConfig.getClientSecret());
|
||||
httpClient.getCredentialsProvider().setCredentials(AuthScope.ANY, credentials);
|
||||
|
||||
/* Alternatively, use form-based auth:
|
||||
*
|
||||
form.add("client_id", serverConfig.getClientId());
|
||||
form.add("client_secret", serverConfig.getClientSecret());
|
||||
*/
|
||||
|
||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate(factory);
|
||||
|
||||
MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
|
||||
form.add("grant_type", "authorization_code");
|
||||
form.add("code", authorizationCode);
|
||||
|
||||
String redirectUri = getStoredSessionString(session, REDIRECT_URI_SESION_VARIABLE);
|
||||
if (redirectUri != null) {
|
||||
form.add("redirect_uri", redirectUri);
|
||||
}
|
||||
|
||||
logger.debug("tokenEndpointURI = " + serverConfig.getTokenEndpointUri());
|
||||
logger.debug("form = " + form);
|
||||
|
||||
String jsonString = null;
|
||||
|
||||
try {
|
||||
jsonString = restTemplate.postForObject(serverConfig.getTokenEndpointUri(), form, String.class);
|
||||
} catch (HttpClientErrorException httpClientErrorException) {
|
||||
|
||||
// Handle error
|
||||
|
||||
logger.error("Token Endpoint error response: "
|
||||
+ httpClientErrorException.getStatusText() + " : "
|
||||
+ httpClientErrorException.getMessage());
|
||||
|
||||
throw new AuthenticationServiceException("Unable to obtain Access Token: " + httpClientErrorException.getMessage());
|
||||
}
|
||||
|
||||
logger.debug("from TokenEndpoint jsonString = " + jsonString);
|
||||
|
||||
JsonElement jsonRoot = new JsonParser().parse(jsonString);
|
||||
if (!jsonRoot.isJsonObject()) {
|
||||
throw new AuthenticationServiceException("Token Endpoint did not return a JSON object: " + jsonRoot);
|
||||
}
|
||||
|
||||
JsonObject tokenResponse = jsonRoot.getAsJsonObject();
|
||||
|
||||
if (tokenResponse.get("error") != null) {
|
||||
|
||||
// Handle error
|
||||
|
||||
String error = tokenResponse.get("error").getAsString();
|
||||
|
||||
logger.error("Token Endpoint returned: " + error);
|
||||
|
||||
throw new AuthenticationServiceException("Unable to obtain Access Token. Token Endpoint returned: " + error);
|
||||
|
||||
} else {
|
||||
|
||||
handleAuthorizationRequest(request, response, oidcServerConfig);
|
||||
// Extract the id_token to insert into the
|
||||
// OIDCAuthenticationToken
|
||||
|
||||
// get out all the token strings
|
||||
String accessTokenValue = null;
|
||||
String idTokenValue = null;
|
||||
String refreshTokenValue = null;
|
||||
|
||||
if (tokenResponse.has("access_token")) {
|
||||
accessTokenValue = tokenResponse.get("access_token").getAsString();
|
||||
} else {
|
||||
throw new AuthenticationServiceException("Token Endpoint did not return an access_token: " + jsonString);
|
||||
}
|
||||
|
||||
if (tokenResponse.has("id_token")) {
|
||||
idTokenValue = tokenResponse.get("id_token").getAsString();
|
||||
} else {
|
||||
logger.error("Token Endpoint did not return an id_token");
|
||||
throw new AuthenticationServiceException("Token Endpoint did not return an id_token");
|
||||
}
|
||||
|
||||
if (tokenResponse.has("refresh_token")) {
|
||||
refreshTokenValue = tokenResponse.get("refresh_token").getAsString();
|
||||
}
|
||||
|
||||
try {
|
||||
SignedJWT idToken = SignedJWT.parse(idTokenValue);
|
||||
|
||||
// validate our ID Token over a number of tests
|
||||
ReadOnlyJWTClaimsSet idClaims = idToken.getJWTClaimsSet();
|
||||
|
||||
// check the signature
|
||||
JwtSigningAndValidationService jwtValidator = validationServices.get(serverConfig.getJwksUri());
|
||||
if (jwtValidator != null) {
|
||||
if(!jwtValidator.validateSignature(idToken)) {
|
||||
throw new AuthenticationServiceException("Signature validation failed");
|
||||
}
|
||||
} else {
|
||||
logger.info("No validation service found. Skipping signature validation");
|
||||
}
|
||||
|
||||
// check the issuer
|
||||
if (idClaims.getIssuer() == null) {
|
||||
throw new AuthenticationServiceException("Id Token Issuer is null");
|
||||
} else if (!idClaims.getIssuer().equals(serverConfig.getIssuer())){
|
||||
throw new AuthenticationServiceException("Issuers do not match, expected " + serverConfig.getIssuer() + " got " + idClaims.getIssuer());
|
||||
}
|
||||
|
||||
// check expiration
|
||||
if (idClaims.getExpirationTime() == null) {
|
||||
throw new AuthenticationServiceException("Id Token does not have required expiration claim");
|
||||
} else {
|
||||
// it's not null, see if it's expired
|
||||
Date now = new Date(System.currentTimeMillis() - (timeSkewAllowance * 1000));
|
||||
if (now.after(idClaims.getExpirationTime())) {
|
||||
throw new AuthenticationServiceException("Id Token is expired: " + idClaims.getExpirationTime());
|
||||
}
|
||||
}
|
||||
|
||||
// check not before
|
||||
if (idClaims.getNotBeforeTime() != null) {
|
||||
Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000));
|
||||
if (now.before(idClaims.getNotBeforeTime())){
|
||||
throw new AuthenticationServiceException("Id Token not valid untill: " + idClaims.getNotBeforeTime());
|
||||
}
|
||||
}
|
||||
|
||||
// check issued at
|
||||
if (idClaims.getIssueTime() == null) {
|
||||
throw new AuthenticationServiceException("Id Token does not have required issued-at claim");
|
||||
} else {
|
||||
// since it's not null, see if it was issued in the future
|
||||
Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000));
|
||||
if (now.before(idClaims.getIssueTime())) {
|
||||
throw new AuthenticationServiceException("Id Token was issued in the future: " + idClaims.getIssueTime());
|
||||
}
|
||||
}
|
||||
|
||||
// check audience
|
||||
if (idClaims.getAudience() == null) {
|
||||
throw new AuthenticationServiceException("Id token audience is null");
|
||||
} else if (!idClaims.getAudience().contains(clientConfig.getClientId())) {
|
||||
throw new AuthenticationServiceException("Audience does not match, expected " + clientConfig.getClientId() + " got " + idClaims.getAudience());
|
||||
}
|
||||
|
||||
// compare the nonce to our stored claim
|
||||
// FIXME: Nimbus claims as strings?
|
||||
String nonce = (String) idClaims.getCustomClaim("nonce");
|
||||
if (StringUtils.isBlank(nonce)) {
|
||||
|
||||
logger.error("ID token did not contain a nonce claim.");
|
||||
|
||||
throw new AuthenticationServiceException("ID token did not contain a nonce claim.");
|
||||
}
|
||||
|
||||
String storedNonce = getStoredNonce(session);
|
||||
if (!nonce.equals(storedNonce)) {
|
||||
logger.error("Possible replay attack detected! The comparison of the nonce in the returned "
|
||||
+ "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + ".");
|
||||
|
||||
throw new AuthenticationServiceException(
|
||||
"Possible replay attack detected! The comparison of the nonce in the returned "
|
||||
+ "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + ".");
|
||||
}
|
||||
|
||||
// pull the subject (user id) out as a claim on the id_token
|
||||
|
||||
String userId = idClaims.getSubject();
|
||||
|
||||
// construct an OIDCAuthenticationToken and return a Authentication object w/the userId and the idToken
|
||||
|
||||
OIDCAuthenticationToken token = new OIDCAuthenticationToken(userId, idClaims.getIssuer(), serverConfig, idTokenValue, accessTokenValue, refreshTokenValue);
|
||||
|
||||
Authentication authentication = this.getAuthenticationManager().authenticate(token);
|
||||
|
||||
return authentication;
|
||||
} catch (ParseException e) {
|
||||
throw new AuthenticationServiceException("Couldn't parse idToken: ", e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setAuthorizationEndpointUrl(String authorizationEndpointUrl) {
|
||||
oidcServerConfig.setAuthorizationEndpointUrl(authorizationEndpointUrl);
|
||||
}
|
||||
|
||||
public void setTokenEndpointUrl(String tokenEndpointUrl) {
|
||||
oidcServerConfig.setTokenEndpointUrl(tokenEndpointUrl);
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
oidcServerConfig.setClientId(clientId);
|
||||
}
|
||||
|
||||
public void setClientSecret(String clientSecret) {
|
||||
oidcServerConfig.setClientSecret(clientSecret);
|
||||
}
|
||||
|
||||
public void setX509EncryptUrl(String x509EncryptUrl) {
|
||||
oidcServerConfig.setX509EncryptUrl(x509EncryptUrl);
|
||||
}
|
||||
|
||||
public void setX509SigningUrl(String x509SigningUrl) {
|
||||
oidcServerConfig.setX509SigningUrl(x509SigningUrl);
|
||||
}
|
||||
|
||||
public void setJwkEncryptUrl(String jwkEncryptUrl) {
|
||||
oidcServerConfig.setJwkEncryptUrl(jwkEncryptUrl);
|
||||
}
|
||||
|
||||
public void setJwkSigningUrl(String jwkSigningUrl) {
|
||||
oidcServerConfig.setJwkSigningUrl(jwkSigningUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param issuer
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setIssuer(java.lang.String)
|
||||
*/
|
||||
public void setIssuer(String issuer) {
|
||||
oidcServerConfig.setIssuer(issuer);
|
||||
}
|
||||
* Handle Authorization Endpoint error
|
||||
*
|
||||
* @param request
|
||||
* The request from which to extract parameters and handle the
|
||||
* error
|
||||
* @param response
|
||||
* The response, needed to do a redirect to display the error
|
||||
* @throws IOException
|
||||
* If an input or output exception occurs
|
||||
*/
|
||||
protected void handleError(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||
|
||||
String error = request.getParameter("error");
|
||||
String errorDescription = request.getParameter("error_description");
|
||||
String errorURI = request.getParameter("error_uri");
|
||||
|
||||
throw new AuthenticationServiceException("Error from Authorization Endpoint: " + error + " " + errorDescription + " " + errorURI);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userInfoUrl
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setUserInfoUrl(java.lang.String)
|
||||
*/
|
||||
public void setUserInfoUrl(String userInfoUrl) {
|
||||
oidcServerConfig.setUserInfoUrl(userInfoUrl);
|
||||
}
|
||||
* Get the named stored session variable as a string. Return null if not found or not a string.
|
||||
* @param session
|
||||
* @param key
|
||||
* @return
|
||||
*/
|
||||
private static String getStoredSessionString(HttpSession session, String key) {
|
||||
Object o = session.getAttribute(key);
|
||||
if (o != null && o instanceof String) {
|
||||
return o.toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cryptographically random nonce and store it in the session
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
protected static String createNonce(HttpSession session) {
|
||||
String nonce = new BigInteger(50, new SecureRandom()).toString(16);
|
||||
session.setAttribute(NONCE_SESSION_VARIABLE, nonce);
|
||||
|
||||
return nonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nonce we stored in the session
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
protected static String getStoredNonce(HttpSession session) {
|
||||
return getStoredSessionString(session, NONCE_SESSION_VARIABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cryptographically random state and store it in the session
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
protected static String createState(HttpSession session) {
|
||||
String state = new BigInteger(50, new SecureRandom()).toString(16);
|
||||
session.setAttribute(STATE_SESSION_VARIABLE, state);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state we stored in the session
|
||||
* @param session
|
||||
* @return
|
||||
*/
|
||||
protected static String getStoredState(HttpSession session) {
|
||||
return getStoredSessionString(session, STATE_SESSION_VARIABLE);
|
||||
}
|
||||
|
||||
public int getTimeSkewAllowance() {
|
||||
return timeSkewAllowance;
|
||||
}
|
||||
|
||||
public void setTimeSkewAllowance(int timeSkewAllowance) {
|
||||
this.timeSkewAllowance = timeSkewAllowance;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ package org.mitre.openid.connect.client;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.mitre.openid.connect.config.OIDCServerConfiguration;
|
||||
import org.mitre.openid.connect.config.ServerConfiguration;
|
||||
import org.mitre.openid.connect.model.UserInfo;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
@ -41,7 +41,7 @@ public class OIDCAuthenticationToken extends AbstractAuthenticationToken {
|
|||
private final String issuer; // issuer URL (parsed from the id token)
|
||||
private final String userId; // user id (parsed from the id token)
|
||||
|
||||
private final transient OIDCServerConfiguration serverConfiguration; // server configuration used to fulfill this token, don't serialize it
|
||||
private final transient ServerConfiguration serverConfiguration; // server configuration used to fulfill this token, don't serialize it
|
||||
private final transient UserInfo userInfo; // user info container, don't serialize it b/c it might be huge and can be re-fetched
|
||||
|
||||
/**
|
||||
|
@ -84,7 +84,7 @@ public class OIDCAuthenticationToken extends AbstractAuthenticationToken {
|
|||
* @param idToken
|
||||
*/
|
||||
public OIDCAuthenticationToken(String userId, String issuer,
|
||||
OIDCServerConfiguration serverConfiguration,
|
||||
ServerConfiguration serverConfiguration,
|
||||
String idTokenValue, String accessTokenValue, String refreshTokenValue) {
|
||||
|
||||
super(new ArrayList<GrantedAuthority>(0));
|
||||
|
@ -153,7 +153,7 @@ public class OIDCAuthenticationToken extends AbstractAuthenticationToken {
|
|||
/**
|
||||
* @return the serverConfiguration
|
||||
*/
|
||||
public OIDCServerConfiguration getServerConfiguration() {
|
||||
public ServerConfiguration getServerConfiguration() {
|
||||
return serverConfiguration;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,168 +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.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.mitre.openid.connect.config.OIDCServerConfiguration;
|
||||
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);
|
||||
|
||||
try {
|
||||
return handleAuthorizationGrantResponse(request.getParameter("code"), request, oidcServerConfigs.get(issuerCookie.getValue()));
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} 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(request, 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", 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;
|
||||
}
|
||||
}
|
|
@ -1,256 +0,0 @@
|
|||
package org.mitre.openid.connect.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
|
||||
import org.mitre.openid.connect.config.OIDCServerConfiguration;
|
||||
import org.mitre.openid.connect.view.JwkKeyListView;
|
||||
import org.mitre.openid.connect.view.X509CertificateView;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
|
||||
public class OIDCSignedRequestFilter extends AbstractOIDCAuthenticationFilter {
|
||||
|
||||
private OIDCServerConfiguration oidcServerConfig;
|
||||
|
||||
private JwtSigningAndValidationService signingAndValidationService;
|
||||
|
||||
protected OIDCSignedRequestFilter() {
|
||||
super();
|
||||
|
||||
oidcServerConfig = new OIDCServerConfiguration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
super.afterPropertiesSet();
|
||||
|
||||
// Validating configuration
|
||||
|
||||
Assert.notNull(oidcServerConfig.getAuthorizationEndpointUrl(),
|
||||
"An Authorization Endpoint URI must be supplied");
|
||||
|
||||
Assert.notNull(oidcServerConfig.getTokenEndpointUrl(),
|
||||
"A Token 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");
|
||||
|
||||
}
|
||||
|
||||
@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"))) {
|
||||
|
||||
try {
|
||||
return handleAuthorizationGrantResponse(request.getParameter("code"), request, oidcServerConfig);
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
handleAuthorizationRequest(request, response, oidcServerConfig);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
|
||||
OIDCServerConfiguration serverConfiguration) throws IOException {
|
||||
|
||||
SignedJWT jwt = createAndSignRequestJwt(request, response, serverConfiguration);
|
||||
|
||||
Map<String, String> urlVariables = new HashMap<String, String>();
|
||||
|
||||
urlVariables.put("request", jwt.serialize());
|
||||
|
||||
String authRequest = AbstractOIDCAuthenticationFilter.buildURL(serverConfiguration.getAuthorizationEndpointUrl(), urlVariables);
|
||||
|
||||
logger.debug("Auth Request: " + authRequest);
|
||||
|
||||
response.sendRedirect(authRequest);
|
||||
}
|
||||
|
||||
public SignedJWT createAndSignRequestJwt(HttpServletRequest request, HttpServletResponse response, OIDCServerConfiguration serverConfiguration) {
|
||||
HttpSession session = request.getSession();
|
||||
|
||||
JWTClaimsSet claims = new JWTClaimsSet();
|
||||
|
||||
//set parameters to JwtHeader
|
||||
// header.setAlgorithm(JwsAlgorithm.getByName(SIGNING_ALGORITHM).toString());
|
||||
|
||||
//set parameters to JwtClaims
|
||||
claims.setCustomClaim("response_type", "code");
|
||||
claims.setCustomClaim("client_id", serverConfiguration.getClientId());
|
||||
claims.setCustomClaim("scope", scope);
|
||||
|
||||
// build our redirect URI
|
||||
String redirectUri = buildRedirectURI(request, null);
|
||||
claims.setCustomClaim("redirect_uri", redirectUri);
|
||||
session.setAttribute(REDIRECT_URI_SESION_VARIABLE, redirectUri);
|
||||
|
||||
//create random nonce and state, save them to the session
|
||||
|
||||
String nonce = createNonce(session);
|
||||
claims.setCustomClaim("nonce", nonce);
|
||||
|
||||
String state = createState(session);
|
||||
claims.setCustomClaim("state", state);
|
||||
|
||||
SignedJWT jwt = new SignedJWT(new JWSHeader(serverConfiguration.getSigningAlgorithm()), claims);
|
||||
|
||||
try {
|
||||
signingAndValidationService.signJwt(jwt);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return jwt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the signingAndValidationService
|
||||
*/
|
||||
public JwtSigningAndValidationService getSigningAndValidationService() {
|
||||
return signingAndValidationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param signingAndValidationService the signingAndValidationService to set
|
||||
*/
|
||||
public void setSigningAndValidationService(JwtSigningAndValidationService signingAndValidationService) {
|
||||
this.signingAndValidationService = signingAndValidationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param authorizationEndpointURI
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setAuthorizationEndpointUrl(java.lang.String)
|
||||
*/
|
||||
public void setAuthorizationEndpointUrl(String authorizationEndpointURI) {
|
||||
oidcServerConfig.setAuthorizationEndpointUrl(authorizationEndpointURI);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param clientId
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setClientId(java.lang.String)
|
||||
*/
|
||||
public void setClientId(String clientId) {
|
||||
oidcServerConfig.setClientId(clientId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param issuer
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setIssuer(java.lang.String)
|
||||
*/
|
||||
public void setIssuer(String issuer) {
|
||||
oidcServerConfig.setIssuer(issuer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param clientSecret
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setClientSecret(java.lang.String)
|
||||
*/
|
||||
public void setClientSecret(String clientSecret) {
|
||||
oidcServerConfig.setClientSecret(clientSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tokenEndpointURI
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setTokenEndpointUrl(java.lang.String)
|
||||
*/
|
||||
public void setTokenEndpointUrl(String tokenEndpointURI) {
|
||||
oidcServerConfig.setTokenEndpointUrl(tokenEndpointURI);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x509EncryptUrl
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setX509EncryptUrl(java.lang.String)
|
||||
*/
|
||||
public void setX509EncryptUrl(String x509EncryptUrl) {
|
||||
oidcServerConfig.setX509EncryptUrl(x509EncryptUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x509SigningUrl
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setX509SigningUrl(java.lang.String)
|
||||
*/
|
||||
public void setX509SigningUrl(String x509SigningUrl) {
|
||||
oidcServerConfig.setX509SigningUrl(x509SigningUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param jwkEncryptUrl
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setJwkEncryptUrl(java.lang.String)
|
||||
*/
|
||||
public void setJwkEncryptUrl(String jwkEncryptUrl) {
|
||||
oidcServerConfig.setJwkEncryptUrl(jwkEncryptUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param jwkSigningUrl
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setJwkSigningUrl(java.lang.String)
|
||||
*/
|
||||
public void setJwkSigningUrl(String jwkSigningUrl) {
|
||||
oidcServerConfig.setJwkSigningUrl(jwkSigningUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userInfoUrl
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setUserInfoUrl(java.lang.String)
|
||||
*/
|
||||
public void setUserInfoUrl(String userInfoUrl) {
|
||||
oidcServerConfig.setUserInfoUrl(userInfoUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param algorithmName
|
||||
* @see org.mitre.openid.connect.config.OIDCServerConfiguration#setAlgorithmName(java.lang.String)
|
||||
*/
|
||||
public void setAlgorithmName(String algorithmName) {
|
||||
oidcServerConfig.setAlgorithmName(algorithmName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -26,7 +26,7 @@ public class UserInfoFetcher {
|
|||
form.add("access_token", token.getAccessTokenValue());
|
||||
form.add("schema", "openid");
|
||||
|
||||
String userInfoString = restTemplate.postForObject(token.getServerConfiguration().getUserInfoUrl(), form, String.class);
|
||||
String userInfoString = restTemplate.postForObject(token.getServerConfiguration().getUserInfoUri(), form, String.class);
|
||||
|
||||
JsonObject userInfoJson = new JsonParser().parse(userInfoString).getAsJsonObject();
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package org.mitre.openid.connect.client.service;
|
||||
|
||||
import org.mitre.openid.connect.config.ServerConfiguration;
|
||||
import org.springframework.security.oauth2.provider.ClientDetails;
|
||||
|
||||
/**
|
||||
* @author jricher
|
||||
*
|
||||
*/
|
||||
public interface AuthRequestUrlBuilder {
|
||||
|
||||
/**
|
||||
* @param serverConfig
|
||||
* @param clientConfig
|
||||
* @param redirectUri
|
||||
* @param nonce
|
||||
* @param state
|
||||
* @return
|
||||
*/
|
||||
public String buildAuthRequestUrl(ServerConfiguration serverConfig, ClientDetails clientConfig, String redirectUri, String nonce, String state);
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package org.mitre.openid.connect.client.service;
|
||||
|
||||
import org.springframework.security.oauth2.provider.ClientDetails;
|
||||
|
||||
/**
|
||||
* @author jricher
|
||||
*
|
||||
*/
|
||||
public interface ClientConfigurationService {
|
||||
|
||||
public ClientDetails getClientConfiguration(String issuer);
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package org.mitre.openid.connect.client.service;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
* Gets an issuer for the given request. Might do dynamic discovery, or might be statically configured.
|
||||
*
|
||||
* @author jricher
|
||||
*
|
||||
*/
|
||||
public interface IssuerService {
|
||||
|
||||
public String getIssuer(HttpServletRequest request);
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package org.mitre.openid.connect.client.service;
|
||||
|
||||
import org.mitre.openid.connect.config.ServerConfiguration;
|
||||
|
||||
/**
|
||||
* @author jricher
|
||||
*
|
||||
*/
|
||||
public interface ServerConfigurationService {
|
||||
|
||||
public ServerConfiguration getServerConfiguration(String issuer);
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package org.mitre.openid.connect.client.service.impl;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.mitre.openid.connect.client.service.AuthRequestUrlBuilder;
|
||||
import org.mitre.openid.connect.config.ServerConfiguration;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.oauth2.provider.ClientDetails;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
|
||||
/**
|
||||
*
|
||||
* Builds an auth request redirect URI with normal query parameters.
|
||||
*
|
||||
* @author jricher
|
||||
*
|
||||
*/
|
||||
public class PlainAuthRequestUrlBuilder implements AuthRequestUrlBuilder {
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.mitre.openid.connect.client.service.AuthRequestUrlBuilder#buildAuthRequest(javax.servlet.http.HttpServletRequest, org.mitre.openid.connect.config.ServerConfiguration, org.springframework.security.oauth2.provider.ClientDetails)
|
||||
*/
|
||||
@Override
|
||||
public String buildAuthRequestUrl(ServerConfiguration serverConfig, ClientDetails clientConfig, String redirectUri, String nonce, String state) {
|
||||
try {
|
||||
|
||||
URIBuilder uriBuilder = new URIBuilder(serverConfig.getAuthorizationEndpointUri());
|
||||
uriBuilder.addParameter("response_type", "code");
|
||||
uriBuilder.addParameter("client_id", clientConfig.getClientId());
|
||||
uriBuilder.addParameter("scope", Joiner.on(" ").join(clientConfig.getScope()));
|
||||
|
||||
uriBuilder.addParameter("redirect_uri", redirectUri);
|
||||
|
||||
uriBuilder.addParameter("nonce", nonce);
|
||||
|
||||
uriBuilder.addParameter("state", state);
|
||||
|
||||
// Optional parameters:
|
||||
|
||||
// TODO: display, prompt
|
||||
|
||||
return uriBuilder.build().toString();
|
||||
|
||||
} catch (URISyntaxException e) {
|
||||
throw new AuthenticationServiceException("Malformed Authorization Endpoint Uri", e);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
*
|
||||
*/
|
||||
package org.mitre.openid.connect.client.service.impl;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
|
||||
import org.mitre.openid.connect.client.service.AuthRequestUrlBuilder;
|
||||
import org.mitre.openid.connect.config.ServerConfiguration;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.oauth2.provider.ClientDetails;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
|
||||
/**
|
||||
* @author jricher
|
||||
*
|
||||
*/
|
||||
public class SignedAuthRequestUrlBuilder implements AuthRequestUrlBuilder {
|
||||
|
||||
private JwtSigningAndValidationService signingAndValidationService;
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.mitre.openid.connect.client.service.AuthRequestUrlBuilder#buildAuthRequestUrl(org.mitre.openid.connect.config.ServerConfiguration, org.springframework.security.oauth2.provider.ClientDetails, java.lang.String, java.lang.String, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public String buildAuthRequestUrl(ServerConfiguration serverConfig, ClientDetails clientConfig, String redirectUri, String nonce, String state) {
|
||||
|
||||
// create our signed JWT for the request object
|
||||
JWTClaimsSet claims = new JWTClaimsSet();
|
||||
|
||||
//set parameters to JwtClaims
|
||||
claims.setCustomClaim("response_type", "code");
|
||||
claims.setCustomClaim("client_id", clientConfig.getClientId());
|
||||
claims.setCustomClaim("scope", Joiner.on(" ").join(clientConfig.getScope()));
|
||||
|
||||
// build our redirect URI
|
||||
claims.setCustomClaim("redirect_uri", redirectUri);
|
||||
|
||||
// this comes back in the id token
|
||||
claims.setCustomClaim("nonce", nonce);
|
||||
|
||||
// this comes back in the auth request return
|
||||
claims.setCustomClaim("state", state);
|
||||
|
||||
SignedJWT jwt = new SignedJWT(new JWSHeader(signingAndValidationService.getDefaultSigningAlgorithm()), claims);
|
||||
|
||||
try {
|
||||
signingAndValidationService.signJwt(jwt);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
try {
|
||||
URIBuilder uriBuilder = new URIBuilder(serverConfig.getAuthorizationEndpointUri());
|
||||
uriBuilder.addParameter("request", jwt.serialize());
|
||||
|
||||
// build out the URI
|
||||
return uriBuilder.build().toString();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new AuthenticationServiceException("Malformed Authorization Endpoint Uri", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,7 @@ import org.springframework.test.context.ContextConfiguration;
|
|||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* Unit test for AbstractOIDCAuthenticationFilter
|
||||
* Unit test for OIDCAuthenticationFilter
|
||||
*
|
||||
* @author amanda
|
||||
*
|
||||
|
@ -17,7 +17,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
|||
public class AbstractOIDCAuthenticationFilterTest {
|
||||
|
||||
//@Autowired
|
||||
private AbstractOIDCAuthenticationFilter filter;
|
||||
private OIDCAuthenticationFilter filter;
|
||||
|
||||
//@Test
|
||||
public void testUrlConstruction() {
|
||||
|
@ -27,14 +27,14 @@ public class AbstractOIDCAuthenticationFilterTest {
|
|||
/**
|
||||
* @return the filter
|
||||
*/
|
||||
public AbstractOIDCAuthenticationFilter getFilter() {
|
||||
public OIDCAuthenticationFilter getFilter() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param filter the filter to set
|
||||
*/
|
||||
public void setFilter(AbstractOIDCAuthenticationFilter filter) {
|
||||
public void setFilter(OIDCAuthenticationFilter filter) {
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,186 +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.openid.connect.config;
|
||||
|
||||
import javax.persistence.Basic;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
|
||||
|
||||
/**
|
||||
* @author nemonik
|
||||
*
|
||||
*/
|
||||
public class OIDCServerConfiguration {
|
||||
|
||||
private String authorizationEndpointUrl;
|
||||
|
||||
private String tokenEndpointUrl;
|
||||
|
||||
private String clientSecret;
|
||||
|
||||
private String clientId;
|
||||
|
||||
private String issuer;
|
||||
|
||||
private String x509EncryptUrl;
|
||||
|
||||
private String x509SigningUrl;
|
||||
|
||||
private String jwkEncryptUrl;
|
||||
|
||||
private String jwkSigningUrl;
|
||||
|
||||
private String userInfoUrl;
|
||||
|
||||
private JWSAlgorithm signingAlgorithm;
|
||||
|
||||
public String getAuthorizationEndpointUrl() {
|
||||
return authorizationEndpointUrl;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public String getClientSecret() {
|
||||
return clientSecret;
|
||||
}
|
||||
|
||||
public String getTokenEndpointUrl() {
|
||||
return tokenEndpointUrl;
|
||||
}
|
||||
|
||||
public void setAuthorizationEndpointUrl(String authorizationEndpointURI) {
|
||||
this.authorizationEndpointUrl = authorizationEndpointURI;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public void setIssuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
public void setClientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public void setTokenEndpointUrl(String tokenEndpointURI) {
|
||||
this.tokenEndpointUrl = tokenEndpointURI;
|
||||
}
|
||||
|
||||
public String getX509EncryptUrl() {
|
||||
return x509EncryptUrl;
|
||||
}
|
||||
|
||||
public String getX509SigningUrl() {
|
||||
return x509SigningUrl;
|
||||
}
|
||||
|
||||
public String getJwkEncryptUrl() {
|
||||
return jwkEncryptUrl;
|
||||
}
|
||||
|
||||
public String getJwkSigningUrl() {
|
||||
return jwkSigningUrl;
|
||||
}
|
||||
|
||||
public void setX509EncryptUrl(String x509EncryptUrl) {
|
||||
this.x509EncryptUrl = x509EncryptUrl;
|
||||
}
|
||||
|
||||
public void setX509SigningUrl(String x509SigningUrl) {
|
||||
this.x509SigningUrl = x509SigningUrl;
|
||||
}
|
||||
|
||||
public void setJwkEncryptUrl(String jwkEncryptUrl) {
|
||||
this.jwkEncryptUrl = jwkEncryptUrl;
|
||||
}
|
||||
|
||||
public void setJwkSigningUrl(String jwkSigningUrl) {
|
||||
this.jwkSigningUrl = jwkSigningUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the userInfoUrl
|
||||
*/
|
||||
public String getUserInfoUrl() {
|
||||
return userInfoUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userInfoUrl the userInfoUrl to set
|
||||
*/
|
||||
public void setUserInfoUrl(String userInfoUrl) {
|
||||
this.userInfoUrl = userInfoUrl;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OIDCServerConfiguration [authorizationEndpointUrl=" + authorizationEndpointUrl + ", tokenEndpointUrl=" + tokenEndpointUrl + ", clientSecret=" + clientSecret + ", clientId=" + clientId + ", issuer=" + issuer + ", x509EncryptUrl=" + x509EncryptUrl + ", x509SigningUrl="
|
||||
+ x509SigningUrl + ", jwkEncryptUrl=" + jwkEncryptUrl + ", jwkSigningUrl=" + jwkSigningUrl + ", userInfoUrl=" + userInfoUrl + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the signingAlgorithm
|
||||
*/
|
||||
public JWSAlgorithm getSigningAlgorithm() {
|
||||
return signingAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param signingAlgorithm the signingAlgorithm to set
|
||||
*/
|
||||
public void setSigningAlgorithm(JWSAlgorithm signingAlgorithm) {
|
||||
this.signingAlgorithm = signingAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of this algorithm, return null if no algorithm set.
|
||||
* @return
|
||||
*/
|
||||
@Basic
|
||||
public String getAlgorithmName() {
|
||||
if (signingAlgorithm != null) {
|
||||
return signingAlgorithm.getName();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of this algorithm.
|
||||
* Calls JWSAlgorithm.parse()
|
||||
* @param algorithmName
|
||||
*/
|
||||
public void setAlgorithmName(String algorithmName) {
|
||||
if (algorithmName != null) {
|
||||
signingAlgorithm = JWSAlgorithm.parse(algorithmName);
|
||||
} else {
|
||||
signingAlgorithm = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*******************************************************************************
|
||||
* 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.config;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Container class for a client's view of a server's configuration
|
||||
*
|
||||
* @author nemonik, jricher
|
||||
*
|
||||
*/
|
||||
public class ServerConfiguration {
|
||||
|
||||
private String authorizationEndpointUri;
|
||||
|
||||
private String tokenEndpointUri;
|
||||
|
||||
private String issuer;
|
||||
|
||||
private String jwksUri;
|
||||
|
||||
private String userInfoUri;
|
||||
|
||||
/**
|
||||
* @return the authorizationEndpointUri
|
||||
*/
|
||||
public String getAuthorizationEndpointUri() {
|
||||
return authorizationEndpointUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param authorizationEndpointUri the authorizationEndpointUri to set
|
||||
*/
|
||||
public void setAuthorizationEndpointUri(String authorizationEndpointUri) {
|
||||
this.authorizationEndpointUri = authorizationEndpointUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the tokenEndpointUri
|
||||
*/
|
||||
public String getTokenEndpointUri() {
|
||||
return tokenEndpointUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tokenEndpointUri the tokenEndpointUri to set
|
||||
*/
|
||||
public void setTokenEndpointUri(String tokenEndpointUri) {
|
||||
this.tokenEndpointUri = tokenEndpointUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the issuer
|
||||
*/
|
||||
public String getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param issuer the issuer to set
|
||||
*/
|
||||
public void setIssuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the jwksUri
|
||||
*/
|
||||
public String getJwksUri() {
|
||||
return jwksUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param jwksUri the jwksUri to set
|
||||
*/
|
||||
public void setJwksUri(String jwksUri) {
|
||||
this.jwksUri = jwksUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the userInfoUri
|
||||
*/
|
||||
public String getUserInfoUri() {
|
||||
return userInfoUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userInfoUri the userInfoUri to set
|
||||
*/
|
||||
public void setUserInfoUri(String userInfoUri) {
|
||||
this.userInfoUri = userInfoUri;
|
||||
}
|
||||
|
||||
}
|
|
@ -80,7 +80,7 @@ public class JwkKeyListView extends AbstractView {
|
|||
|
||||
BigInteger mod = rsa.getModulus();
|
||||
BigInteger exp = rsa.getPublicExponent();
|
||||
|
||||
|
||||
RSAKey rsaKey = new RSAKey(Base64URL.encode(mod.toByteArray()), Base64URL.encode(exp.toByteArray()), Use.SIGNATURE, JWSAlgorithm.RS256, keyId);
|
||||
|
||||
jwks.add(rsaKey);
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.Map;
|
|||
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
|
@ -28,29 +29,21 @@ import org.springframework.web.servlet.ModelAndView;
|
|||
public class JsonWebKeyEndpoint {
|
||||
|
||||
@Autowired
|
||||
JwtSigningAndValidationService jwtService;
|
||||
private JwtSigningAndValidationService jwtService;
|
||||
|
||||
@RequestMapping(value = "/jwk", produces = "application/json")
|
||||
public ModelAndView getJwk() {
|
||||
public String getJwk(Model m) {
|
||||
|
||||
// map from key id to signer
|
||||
Map<String, PublicKey> keys = jwtService.getAllPublicKeys();
|
||||
|
||||
// TODO: check if keys are empty, return a 404 here or just an empty list?
|
||||
|
||||
return new ModelAndView("jwkKeyList", "keys", keys);
|
||||
m.addAttribute("keys", keys);
|
||||
|
||||
return "jwkKeyList";
|
||||
}
|
||||
|
||||
@RequestMapping("/x509")
|
||||
public ModelAndView getX509() {
|
||||
// map from key id to signer
|
||||
Map<String, PublicKey> keys = jwtService.getAllPublicKeys();
|
||||
|
||||
// TODO: check if keys are empty, return a 404 here or just an empty list?
|
||||
|
||||
return new ModelAndView("x509certs", "keys", keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the jwtService
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue