saml url support

This commit is contained in:
Bill Burke 2015-02-10 11:33:18 -05:00
parent 2a662ad2a6
commit 0e9fcf19e0
6 changed files with 151 additions and 38 deletions

View file

@ -679,7 +679,7 @@ module.controller('ApplicationDetailCtrl', function($scope, realm, application,
$scope.application.protocol = $scope.protocol;
$scope.application.attributes['saml.signature.algorithm'] = $scope.signatureAlgorithm;
if (!$scope.application.bearerOnly && (!$scope.application.redirectUris || $scope.application.redirectUris.length == 0)) {
if ($scope.application.protocol != 'saml' && !$scope.application.bearerOnly && (!$scope.application.redirectUris || $scope.application.redirectUris.length == 0)) {
Notifications.error("You must specify at least one redirect uri");
} else {
if ($scope.create) {

View file

@ -56,6 +56,20 @@
</div>
<span tooltip-placement="right" tooltip="'Confidential' applications require a secret to initiate login protocol. 'Public' clients do not require a secret. 'Bearer-only' applications are web services that never initiate a login." class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-sm-2 control-label" for="logoutPostBinding">Assertion Consumer Service POST Binding URL</label>
<div class="col-sm-6">
<input ng-model="application.attributes.saml_assertion_consumer_service_url_post" class="form-control" type="text" name="logoutPostBinding" id="logoutPostBinding" />
</div>
<span tooltip-placement="right" tooltip="SAML POST Binding URL for the application's single logout service (login responses. If you leave this blank then the consumer URL is expected be sent with authn request from the SP. This sent URL must be verified via the redirect URI pattern config option below." class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-sm-2 control-label" for="logoutPostBinding">Assertion Consumer Service Redirect Binding URL</label>
<div class="col-sm-6">
<input ng-model="application.attributes.saml_assertion_consumer_service_url_redirect" class="form-control" type="text" name="logoutRedirectBinding" id="logoutRedirectBinding" />
</div>
<span tooltip-placement="right" tooltip="SAML Redirect Binding URL for the application's assertion consumer service (login responses). If you leave this blank then the consumer URL is expected be sent with authn request from the SP. This sent URL must be verified via the redirect URI pattern config option below." class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-sm-2 control-label" for="samlServerSignature">Include AuthnStatement</label>
<div class="col-sm-6">
@ -125,8 +139,22 @@
</div>
<span tooltip-placement="right" tooltip="When true, logout requires a browser redirect to application. When false, server performs a background invocation for logout." class="fa fa-info-circle"></span>
</div>
<div class="form-group" data-ng-show="!application.bearerOnly">
<label class="col-sm-2 control-label" for="newRedirectUri">Redirect URI <span class="required" data-ng-show="create">*</span></label>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-sm-2 control-label" for="logoutPostBinding">Logout Service POST Binding URL</label>
<div class="col-sm-6">
<input ng-model="application.attributes.saml_single_logout_service_url_post" class="form-control" type="text" name="logoutPostBinding" id="logoutPostBinding" />
</div>
<span tooltip-placement="right" tooltip="SAML POST Binding URL for the application's single logout service. You can leave this blank if you are using a differen binding" class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-sm-2 control-label" for="logoutPostBinding">Logout Service Redirect Binding URL</label>
<div class="col-sm-6">
<input ng-model="application.attributes.saml_single_logout_service_url_redirect" class="form-control" type="text" name="logoutRedirectBinding" id="logoutRedirectBinding" />
</div>
<span tooltip-placement="right" tooltip="SAML Redirect Binding URL for the application's single logout service. You can leave this blank if you are using a different binding." class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix block" data-ng-show="!application.bearerOnly">
<label class="col-sm-2 control-label" for="newRedirectUri">Redirect URI <span class="required" data-ng-show="create && protocol != 'saml'">*</span></label>
<div class="col-sm-6 multiple" ng-repeat="redirectUri in application.redirectUris">
<div class="input-group kc-item-deletable">
<input class="form-control" type="text" data-ng-class="{'input-below':!$first}"
@ -157,7 +185,7 @@
</div>
<span tooltip-placement="right" tooltip="Default URL to use when no redirect URI is specified. This URL will also be used when the auth server needs to link to the application for any reason." class="fa fa-info-circle"></span>
</div>
<div class="form-group" data-ng-hide="create">
<div class="form-group" data-ng-hide="create || protocol == 'saml'">
<label class="col-sm-2 control-label" for="adminUrl">Admin URL</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="adminUrl" id="adminUrl"

View file

@ -47,10 +47,14 @@ public class SamlProtocol implements LoginProtocol {
public static final String SAML_SIGNING_CERTIFICATE_ATTRIBUTE = "saml.signing." + ClientAttributeCertificateResource.X509CERTIFICATE;
public static final String SAML_ENCRYPTION_CERTIFICATE_ATTRIBUTE = "saml.encryption." + ClientAttributeCertificateResource.X509CERTIFICATE;
public static final String SAML_CLIENT_SIGNATURE_ATTRIBUTE = "saml.client.signature";
public static final String SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE = "saml_assertion_consumer_url_post";
public static final String SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE = "saml_assertion_consumer_url_redirect";
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE = "saml_single_logout_service_url_post";
public static final String SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE = "saml_single_logout_service_url_redirect";
public static final String LOGIN_PROTOCOL = "saml";
public static final String SAML_BINDING = "saml_binding";
public static final String SAML_POST_BINDING = "post";
public static final String SAML_GET_BINDING = "get";
public static final String SAML_REDIRECT_BINDING = "get";
public static final String SAML_SERVER_SIGNATURE = "saml.server.signature";
public static final String SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
public static final String SAML_AUTHNSTATEMENT = "saml.authnstatement";
@ -129,7 +133,7 @@ public class SamlProtocol implements LoginProtocol {
protected boolean isPostBinding(ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || "true".equals(client.getAttribute(SAML_FORCE_POST_BINDING));
return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || forcePostBinding(client);
}
protected boolean isLogoutPostBindingForInitiator(UserSessionModel session) {
@ -137,8 +141,36 @@ public class SamlProtocol implements LoginProtocol {
return SamlProtocol.SAML_POST_BINDING.equals(note);
}
protected boolean isLogoutPostBindingForClient(ClientModel client) {
return SamlProtocol.SAML_POST_BINDING.equals(client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING));
protected boolean isLogoutPostBindingForClient(ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
String logoutPostUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
String logoutRedirectUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
if (logoutPostUrl == null) {
// if we don't have a redirect uri either, return true and default to the admin url + POST binding
if (logoutRedirectUrl == null) return true;
return false;
}
if (forcePostBinding(client)) {
return true; // configured to force a post binding and post binding logout url is not null
}
String bindingType = clientSession.getNote(SAML_BINDING);
// if the login binding was POST, return true
if (SAML_POST_BINDING.equals(bindingType)) return true;
if (logoutRedirectUrl == null) return true; // we don't have a redirect binding url, so use post binding
return false; // redirect binding
}
public static boolean forcePostBinding(ClientModel client) {
return "true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING));
}
protected String getNameIdFormat(ClientSessionModel clientSession) {
@ -290,10 +322,15 @@ public class SamlProtocol implements LoginProtocol {
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
}
protected String getBindingUri(ClientModel client) {
String bindingUri = client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING_URI);
if (bindingUri == null ) bindingUri = ((ApplicationModel)client).getManagementUrl();
return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), bindingUri);
public static String getLogoutServiceUrl(UriInfo uriInfo, ClientModel client, String bindingType) {
String logoutServiceUrl = null;
if (SAML_POST_BINDING.equals(bindingType)) {
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
} else {
logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
}
if (logoutServiceUrl == null && client instanceof ApplicationModel) logoutServiceUrl = ((ApplicationModel)client).getManagementUrl();
return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), logoutServiceUrl);
}
@ -301,15 +338,14 @@ public class SamlProtocol implements LoginProtocol {
public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
if (!(client instanceof ApplicationModel)) return null;
ApplicationModel app = (ApplicationModel)client;
String bindingUri = getBindingUri(client);
if (bindingUri == null) return null;
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(clientSession, client);
try {
if (isLogoutPostBindingForClient(app)) {
if (isLogoutPostBindingForClient(clientSession)) {
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
return logoutBuilder.postBinding().request(bindingUri);
} else {
logger.debug("frontchannel redirect binding");
String bindingUri = getLogoutServiceUrl(uriInfo, client, SAML_REDIRECT_BINDING);
return logoutBuilder.redirectBinding().request(bindingUri);
}
} catch (ConfigurationException e) {
@ -360,7 +396,11 @@ public class SamlProtocol implements LoginProtocol {
ClientModel client = clientSession.getClient();
if (!(client instanceof ApplicationModel)) return;
ApplicationModel app = (ApplicationModel)client;
if (app.getManagementUrl() == null) return;
String logoutUrl = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING);
if (logoutUrl == null) {
logger.warnv("Can't do backchannel logout. No SingleLogoutService POST Binding registered for client: {1}", client.getClientId());
return;
}
SAML2LogoutRequestBuilder logoutBuilder = createLogoutRequest(clientSession, client);
@ -373,13 +413,11 @@ public class SamlProtocol implements LoginProtocol {
}
String adminUrl = ResourceAdminManager.getManagementUrl(uriInfo.getRequestUri(), app);
ApacheHttpClient4Executor executor = ResourceAdminManager.createExecutor();
try {
ClientRequest request = executor.createRequest(adminUrl);
ClientRequest request = executor.createRequest(logoutUrl);
request.formParameter(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString);
request.formParameter(SAML2LogOutHandler.BACK_CHANNEL_LOGOUT, SAML2LogOutHandler.BACK_CHANNEL_LOGOUT);
ClientResponse response = null;
@ -387,9 +425,9 @@ public class SamlProtocol implements LoginProtocol {
response = request.post();
response.releaseConnection();
// Undertow will redirect root urls not ending in "/" to root url + "/". Test for this weird behavior
if (response.getStatus() == 302 && !adminUrl.endsWith("/")) {
if (response.getStatus() == 302 && !logoutUrl.endsWith("/")) {
String redirect = (String)response.getHeaders().getFirst(HttpHeaders.LOCATION);
String withSlash = adminUrl + "/";
String withSlash = logoutUrl + "/";
if (withSlash.equals(redirect)) {
request = executor.createRequest(withSlash);
request.formParameter(GeneralConstants.SAML_REQUEST_KEY, logoutRequestString);

View file

@ -210,8 +210,23 @@ public class SamlService {
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
String bindingType = getBindingType(requestAbstractType);
if ("true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING))) bindingType = SamlProtocol.SAML_POST_BINDING;
String redirect = null;
URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL();
String redirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client);
if (redirectUri != null && !"null".equals(redirectUri)) { // "null" is for testing purposes
redirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client);
} else {
if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
} else {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
}
if (redirect == null && client instanceof ApplicationModel) {
redirect = ((ApplicationModel)client).getManagementUrl();
}
}
if (redirect == null) {
event.error(Errors.INVALID_REDIRECT_URI);
@ -224,7 +239,7 @@ public class SamlService {
clientSession.setRedirectUri(redirect);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE);
clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
clientSession.setNote(SamlProtocol.SAML_BINDING, getBindingType(requestAbstractType));
clientSession.setNote(SamlProtocol.SAML_BINDING, bindingType);
clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
clientSession.setNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());
@ -269,7 +284,7 @@ public class SamlService {
if (JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get().equals(requestedProtocolBinding.toString())) {
return SamlProtocol.SAML_POST_BINDING;
} else {
return SamlProtocol.SAML_GET_BINDING;
return SamlProtocol.SAML_REDIRECT_BINDING;
}
}
@ -294,9 +309,9 @@ public class SamlService {
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
if (authResult != null) {
String bindingUri = client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING_URI);
if (bindingUri == null ) bindingUri = ((ApplicationModel)client).getManagementUrl();
bindingUri = ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), bindingUri);
String logoutBinding = getBindingType();
if ("true".equals(client.getAttribute(SamlProtocol.SAML_FORCE_POST_BINDING))) logoutBinding = SamlProtocol.SAML_POST_BINDING;
String bindingUri = SamlProtocol.getLogoutServiceUrl(uriInfo, client, logoutBinding);
UserSessionModel userSession = authResult.getSession();
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
if (SamlProtocol.requiresRealmSignature(client)) {
@ -305,8 +320,6 @@ public class SamlService {
}
if (relayState != null) userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
String logoutBinding = client.getAttribute(SamlProtocol.SAML_LOGOUT_BINDING);
if (logoutBinding == null) logoutBinding = getBindingType();
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
userSession.setNote(SamlProtocol.SAML_LOGOUT_ISSUER, logoutRequest.getIssuer().getValue());
userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL);
@ -440,7 +453,7 @@ public class SamlService {
@Override
protected String getBindingType() {
return SamlProtocol.SAML_GET_BINDING;
return SamlProtocol.SAML_REDIRECT_BINDING;
}

View file

@ -192,12 +192,20 @@ public class SamlBindingTest {
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/employee-sig-front/");
Assert.assertTrue(driver.getPageSource().contains("bburke"));
// visit 3rd app
System.out.println("visit 3rd app ");
driver.navigate().to("http://localhost:8081/sales-post-sig/");
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/sales-post-sig/");
Assert.assertTrue(driver.getPageSource().contains("bburke"));
// logout of first app
System.out.println("GLO");
driver.navigate().to("http://localhost:8081/employee-sig?GLO=true");
checkLoggedOut("http://localhost:8081/employee-sig/");
driver.navigate().to("http://localhost:8081/employee-sig-front/");
Assert.assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/demo/protocol/saml"));
driver.navigate().to("http://localhost:8081/sales-post-sig/");
Assert.assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/demo/protocol/saml"));
}

View file

@ -34,12 +34,15 @@
"fullScopeAllowed": true,
"protocol": "saml",
"baseUrl": "http://localhost:8081/sales-post",
"adminUrl": "http://localhost:8081/sales-post",
"redirectUris": [
"http://localhost:8081/sales-post/*"
],
"attributes": {
"saml.authnstatement": "true"
"saml.authnstatement": "true",
"saml_assertion_consumer_url_post": "http://localhost:8081/sales-post",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post",
"saml_single_logout_service_url_post": "http://localhost:8081/sales-post",
"saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post"
}
},
{
@ -48,11 +51,14 @@
"protocol": "saml",
"fullScopeAllowed": true,
"baseUrl": "http://localhost:8081/sales-post-sig",
"adminUrl": "http://localhost:8081/sales-post-sig",
"redirectUris": [
"http://localhost:8081/sales-post-sig/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig",
"saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig",
"saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig",
"saml.server.signature": "true",
"saml.signature.algorithm": "RSA_SHA256",
"saml.client.signature": "true",
@ -72,6 +78,10 @@
"http://localhost:8081/sales-post-sig-transient/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig-transient",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig-transient",
"saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig-transient",
"saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig-transient",
"saml.server.signature": "true",
"saml.signature.algorithm": "RSA_SHA256",
"saml.client.signature": "true",
@ -86,11 +96,14 @@
"protocol": "saml",
"fullScopeAllowed": true,
"baseUrl": "http://localhost:8081/sales-post-sig-persistent",
"adminUrl": "http://localhost:8081/sales-post-sig-persistent",
"redirectUris": [
"http://localhost:8081/sales-post-sig-persistent/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig-persistent",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig-persistent",
"saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig-persistent",
"saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig-persistent",
"saml.server.signature": "true",
"saml.signature.algorithm": "RSA_SHA256",
"saml.client.signature": "true",
@ -110,6 +123,10 @@
"http://localhost:8081/sales-post-sig-email/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig-email",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig-email",
"saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig-email",
"saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig-email",
"saml.server.signature": "true",
"saml.signature.algorithm": "RSA_SHA256",
"saml.client.signature": "true",
@ -160,11 +177,14 @@
"protocol": "saml",
"fullScopeAllowed": true,
"baseUrl": "http://localhost:8081/sales-post-enc",
"adminUrl": "http://localhost:8081/sales-post-enc",
"redirectUris": [
"http://localhost:8081/sales-post-enc/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-enc",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-enc",
"saml_single_logout_service_url_post": "http://localhost:8081/sales-post-enc",
"saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-enc",
"saml.server.signature": "true",
"saml.signature.algorithm": "RSA_SHA512",
"saml.client.signature": "true",
@ -182,11 +202,14 @@
"protocol": "saml",
"fullScopeAllowed": true,
"baseUrl": "http://localhost:8081/employee-sig",
"adminUrl": "http://localhost:8081/employee-sig",
"redirectUris": [
"http://localhost:8081/employee-sig/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8081/employee-sig",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/employee-sig",
"saml_single_logout_service_url_post": "http://localhost:8081/employee-sig",
"saml_single_logout_service_url_redirect": "http://localhost:8081/employee-sig",
"saml.server.signature": "true",
"saml.client.signature": "true",
"saml.signature.algorithm": "RSA_SHA1",
@ -202,11 +225,14 @@
"fullScopeAllowed": true,
"frontchannelLogout": true,
"baseUrl": "http://localhost:8081/employee-sig-front",
"adminUrl": "http://localhost:8081/employee-sig-front",
"redirectUris": [
"http://localhost:8081/employee-sig-front/*"
],
"attributes": {
"saml_assertion_consumer_url_post": "http://localhost:8081/employee-sig-front",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/employee-sig-front",
"saml_single_logout_service_url_post": "http://localhost:8081/employee-sig-front",
"saml_single_logout_service_url_redirect": "http://localhost:8081/employee-sig-front",
"saml.server.signature": "true",
"saml.client.signature": "true",
"saml.signature.algorithm": "RSA_SHA1",